mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 03:05:42 +00:00
feat(backoffice): guard routes and actions by permission
Mirrors the new keys in lib/permissions.ts, wraps the warehouse, overview, reports, support and booking-request routes in RequirePermission, and gates the dispatch, mark-paid, invoice pay/cancel, export and support-send actions behind their own keys. Removes duplicate route blocks.
This commit is contained in:
@@ -164,7 +164,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
label: "Reports",
|
label: "Reports",
|
||||||
href: "/dashboard/reports",
|
href: "/dashboard/reports",
|
||||||
icon: <BarChart3 />,
|
icon: <BarChart3 />,
|
||||||
permission: FREIGHT_PERMS.bookings.view,
|
permission: FREIGHT_PERMS.reports.view,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Customers",
|
label: "Customers",
|
||||||
@@ -196,19 +196,19 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
label: "Payments",
|
label: "Payments",
|
||||||
href: "/dashboard/payments",
|
href: "/dashboard/payments",
|
||||||
icon: <Wallet />,
|
icon: <Wallet />,
|
||||||
permission: FREIGHT_PERMS.bookings.view,
|
permission: FREIGHT_PERMS.payments.view,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Invoices",
|
label: "Invoices",
|
||||||
href: "/dashboard/invoices",
|
href: "/dashboard/invoices",
|
||||||
icon: <Receipt />,
|
icon: <Receipt />,
|
||||||
permission: FREIGHT_PERMS.bookings.view,
|
permission: FREIGHT_PERMS.invoices.view,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Support",
|
label: "Support",
|
||||||
href: "/dashboard/support",
|
href: "/dashboard/support",
|
||||||
icon: <LifeBuoy />,
|
icon: <LifeBuoy />,
|
||||||
permission: FREIGHT_PERMS.support.view,
|
permission: FREIGHT_PERMS.support.agentView,
|
||||||
},
|
},
|
||||||
...demoItems,
|
...demoItems,
|
||||||
],
|
],
|
||||||
@@ -844,26 +844,30 @@ const App = () => {
|
|||||||
element={<Navigate to="/dashboard/overview" replace />}
|
element={<Navigate to="/dashboard/overview" replace />}
|
||||||
/>
|
/>
|
||||||
<Route path="/dashboard" element={<DashboardShell />}>
|
<Route path="/dashboard" element={<DashboardShell />}>
|
||||||
<Route path="overview" element={<OverviewPage />} />
|
<Route path="overview" element={<RequirePermission permission={FREIGHT_PERMS.overview.view}><OverviewPage /></RequirePermission>} />
|
||||||
<Route path="reports" element={<ReportsHubPage />} />
|
<Route path="reports" element={<RequirePermission permission={FREIGHT_PERMS.reports.view}><ReportsHubPage /></RequirePermission>} />
|
||||||
<Route path="reports/:reportKey" element={<ReportPage />} />
|
<Route path="reports/:reportKey" element={<RequirePermission permission={FREIGHT_PERMS.reports.view}><ReportPage /></RequirePermission>} />
|
||||||
{/* Dev/testing page for the mock AI booking assistant. */}
|
{/* Dev/testing page for the mock AI booking assistant. */}
|
||||||
<Route
|
<Route
|
||||||
path="ai-booking-mock-test"
|
path="ai-booking-mock-test"
|
||||||
element={<AiBookingMockTestPage />}
|
element={
|
||||||
|
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||||
|
<AiBookingMockTestPage />
|
||||||
|
</RequirePermission>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="profile" element={<MyProfilePage />} />
|
<Route path="profile" element={<MyProfilePage />} />
|
||||||
|
|
||||||
<Route path="booking-requests" element={<BookingRequestsPage />} />
|
<Route path="booking-requests" element={<RequirePermission permission={FREIGHT_PERMS.bookings.view}><BookingRequestsPage /></RequirePermission>} />
|
||||||
<Route
|
<Route
|
||||||
path="payments"
|
path="payments"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
<RequirePermission permission={FREIGHT_PERMS.payments.view}>
|
||||||
<PaymentsPage />
|
<PaymentsPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="support" element={<SupportInboxPage />} />
|
<Route path="support" element={<RequirePermission permission={FREIGHT_PERMS.support.agentView}><SupportInboxPage /></RequirePermission>} />
|
||||||
<Route
|
<Route
|
||||||
path="customers"
|
path="customers"
|
||||||
element={
|
element={
|
||||||
@@ -883,7 +887,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="invoices"
|
path="invoices"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
<RequirePermission permission={FREIGHT_PERMS.invoices.view}>
|
||||||
<InvoicesPage />
|
<InvoicesPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -891,19 +895,27 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="invoices/:id"
|
path="invoices/:id"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
<RequirePermission permission={FREIGHT_PERMS.invoices.view}>
|
||||||
<InvoiceDetailPage />
|
<InvoiceDetailPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="booking-requests/new" element={<NewBookingPage />} />
|
<Route path="booking-requests/new" element={<RequirePermission permission={FREIGHT_PERMS.bookings.view}><NewBookingPage /></RequirePermission>} />
|
||||||
<Route
|
<Route
|
||||||
path="booking-requests/:id"
|
path="booking-requests/:id"
|
||||||
element={<BookingRequestDetailPage />}
|
element={
|
||||||
|
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||||
|
<BookingRequestDetailPage />
|
||||||
|
</RequirePermission>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="booking-requests/:id/contract"
|
path="booking-requests/:id/contract"
|
||||||
element={<BookingContractPage />}
|
element={
|
||||||
|
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||||
|
<BookingContractPage />
|
||||||
|
</RequirePermission>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{/* Legacy booking-based clearance URLs → the contract clearance hub. */}
|
{/* Legacy booking-based clearance URLs → the contract clearance hub. */}
|
||||||
<Route
|
<Route
|
||||||
@@ -1091,44 +1103,26 @@ const App = () => {
|
|||||||
path="bookings/:id/milestones"
|
path="bookings/:id/milestones"
|
||||||
element={<BookingMilestonesRedirect />}
|
element={<BookingMilestonesRedirect />}
|
||||||
/>
|
/>
|
||||||
<Route path="warehouses" element={<WarehouseListPage />} />
|
<Route path="warehouses" element={<RequirePermission permission={FREIGHT_PERMS.warehouses.view}><WarehouseListPage /></RequirePermission>} />
|
||||||
<Route path="warehouses/:id" element={<WarehouseDetailPage />} />
|
<Route path="warehouses/:id" element={<RequirePermission permission={FREIGHT_PERMS.warehouses.view}><WarehouseDetailPage /></RequirePermission>} />
|
||||||
<Route
|
<Route path="warehouse-inventory" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><WarehouseInventoryPage /></RequirePermission>} />
|
||||||
path="warehouse-inventory"
|
<Route path="import-warehouse" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ImportWarehouseFlowPage /></RequirePermission>} />
|
||||||
element={<WarehouseInventoryPage />}
|
<Route path="export-warehouse" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ExportWarehouseFlowPage /></RequirePermission>} />
|
||||||
/>
|
<Route path="arrival-queue" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ArrivalQueuePage /></RequirePermission>} />
|
||||||
<Route path="import-warehouse" element={<ImportWarehouseFlowPage />} />
|
<Route path="loading-queue" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><LoadingQueuePage /></RequirePermission>} />
|
||||||
<Route path="export-warehouse" element={<ExportWarehouseFlowPage />} />
|
<Route path="intercity" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><IntercityPage /></RequirePermission>} />
|
||||||
<Route path="arrival-queue" element={<ArrivalQueuePage />} />
|
<Route path="trucks-on-site" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><TrucksOnSitePage /></RequirePermission>} />
|
||||||
<Route path="loading-queue" element={<LoadingQueuePage />} />
|
<Route path="import-trucks" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ImportTrucksPage /></RequirePermission>} />
|
||||||
<Route path="intercity" element={<IntercityPage />} />
|
<Route path="edr-last-mile-returns" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><EDRLastMileReturnsPage /></RequirePermission>} />
|
||||||
<Route path="trucks-on-site" element={<TrucksOnSitePage />} />
|
<Route path="container-returns" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ContainerReturnsPage /></RequirePermission>} />
|
||||||
<Route path="import-trucks" element={<ImportTrucksPage />} />
|
<Route path="loaded-inventory" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><LoadedInventoryPage /></RequirePermission>} />
|
||||||
<Route
|
<Route path="dispatch-queue" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><DispatchQueuePage /></RequirePermission>} />
|
||||||
path="edr-last-mile-returns"
|
<Route path="export-djibouti-unloading" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ExportDjiboutiUnloadingQueuePage /></RequirePermission>} />
|
||||||
element={<EDRLastMileReturnsPage />}
|
<Route path="interchange-documents" element={<RequirePermission permission={FREIGHT_PERMS.interchangeDocuments.view}><InterchangeDocumentsPage /></RequirePermission>} />
|
||||||
/>
|
<Route path="inventory-inquiry" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><InventoryInquiryPage /></RequirePermission>} />
|
||||||
<Route path="container-returns" element={<ContainerReturnsPage />} />
|
<Route path="warehouse-rules" element={<RequirePermission permission={[FREIGHT_PERMS.warehouseAllocationRules.view, FREIGHT_PERMS.warehouseFeeRules.view]}><WarehouseRulesPage /></RequirePermission>} />
|
||||||
<Route path="loaded-inventory" element={<LoadedInventoryPage />} />
|
<Route path="warehouse-fee-invoices" element={<RequirePermission permission={FREIGHT_PERMS.warehouseFeeInvoices.view}><WarehouseInvoicesPage /></RequirePermission>} />
|
||||||
<Route path="dispatch-queue" element={<DispatchQueuePage />} />
|
<Route path="warehouse-dashboard" element={<RequirePermission permission={FREIGHT_PERMS.warehouseDashboard.view}><WarehouseDashboardPage /></RequirePermission>} />
|
||||||
<Route
|
|
||||||
path="export-djibouti-unloading"
|
|
||||||
element={<ExportDjiboutiUnloadingQueuePage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="interchange-documents"
|
|
||||||
element={<InterchangeDocumentsPage />}
|
|
||||||
/>
|
|
||||||
<Route path="inventory-inquiry" element={<InventoryInquiryPage />} />
|
|
||||||
<Route path="warehouse-rules" element={<WarehouseRulesPage />} />
|
|
||||||
<Route
|
|
||||||
path="warehouse-fee-invoices"
|
|
||||||
element={<WarehouseInvoicesPage />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="warehouse-dashboard"
|
|
||||||
element={<WarehouseDashboardPage />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="operations/train-scheduling"
|
path="operations/train-scheduling"
|
||||||
@@ -1336,62 +1330,6 @@ const App = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="operations/train-scheduling"
|
|
||||||
element={
|
|
||||||
<Navigate to="/dashboard/operations/train-scheduling-v2" replace />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="operations/batch-board"
|
|
||||||
element={
|
|
||||||
<RequirePermission permission={FREIGHT_PERMS.trainScheduling.view}>
|
|
||||||
<BatchBoardPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="operations/batch-board/:scheduleId"
|
|
||||||
element={
|
|
||||||
<RequirePermission permission={FREIGHT_PERMS.trainScheduling.view}>
|
|
||||||
<BatchScheduleDetailPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="operations/train-scheduling-v2"
|
|
||||||
element={
|
|
||||||
<RequirePermission permission={FREIGHT_PERMS.trainScheduling.view}>
|
|
||||||
<TrainScheduleV2ListPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="operations/train-scheduling-v2/:scheduleId"
|
|
||||||
element={
|
|
||||||
<RequirePermission permission={FREIGHT_PERMS.trainScheduling.view}>
|
|
||||||
<TrainScheduleV2DetailPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="operations/train-scheduling-v2/:scheduleId/track"
|
|
||||||
element={
|
|
||||||
<RequirePermission permission={FREIGHT_PERMS.trainScheduling.view}>
|
|
||||||
<TrainScheduleTrackPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="routes"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view]}
|
|
||||||
>
|
|
||||||
<RoutesPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="fuel-purchases"
|
path="fuel-purchases"
|
||||||
element={
|
element={
|
||||||
@@ -1472,108 +1410,6 @@ const App = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="locomotives"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[
|
|
||||||
FREIGHT_PERMS.locomotives.view,
|
|
||||||
FREIGHT_PERMS.fleet.view,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<FleetResourcePage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="trains"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
|
|
||||||
>
|
|
||||||
<FleetResourcePage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="trains/:id"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
|
|
||||||
>
|
|
||||||
<TrainDetailPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="train-builder"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
|
|
||||||
>
|
|
||||||
<TrainBuilderListPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="train-builder/:id"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}
|
|
||||||
>
|
|
||||||
<TrainBuilderDetailPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="wagons"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view]}
|
|
||||||
>
|
|
||||||
<FleetResourcePage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="wagon-transfers"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[
|
|
||||||
FREIGHT_PERMS.wagons.transferView,
|
|
||||||
FREIGHT_PERMS.wagons.view,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<WagonTransfersPage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="containers"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[
|
|
||||||
FREIGHT_PERMS.containers.view,
|
|
||||||
FREIGHT_PERMS.fleet.view,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<FleetResourcePage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="cargoes"
|
|
||||||
element={
|
|
||||||
<RequirePermission
|
|
||||||
permission={[
|
|
||||||
FREIGHT_PERMS.cargoes.view,
|
|
||||||
FREIGHT_PERMS.fleet.view,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<FleetResourcePage />
|
|
||||||
</RequirePermission>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Legacy embedded user management routes */}
|
{/* Legacy embedded user management routes */}
|
||||||
{/* <Route path="user-management" element={<UserManagementPage />} />
|
{/* <Route path="user-management" element={<UserManagementPage />} />
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export function ClearanceOpsTabs({
|
|||||||
Boolean(exchangeEntityId) &&
|
Boolean(exchangeEntityId) &&
|
||||||
(hasPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions) ||
|
(hasPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions) ||
|
||||||
hasPermission(user, FREIGHT_PERMS.contracts.clearanceDjActions));
|
hasPermission(user, FREIGHT_PERMS.contracts.clearanceDjActions));
|
||||||
|
// Risk assignment + incident reporting hit bookings:operations endpoints.
|
||||||
|
const canOps = hasPermission(user, FREIGHT_PERMS.bookings.operations);
|
||||||
const hasTabs = (showOpsTabs && hasOps) || showDocuments || showExchange;
|
const hasTabs = (showOpsTabs && hasOps) || showDocuments || showExchange;
|
||||||
|
|
||||||
if (!hasTabs) {
|
if (!hasTabs) {
|
||||||
@@ -98,12 +100,12 @@ export function ClearanceOpsTabs({
|
|||||||
Document exchange
|
Document exchange
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
) : null}
|
) : null}
|
||||||
{showOpsTabs && riskMs ? (
|
{showOpsTabs && canOps && riskMs ? (
|
||||||
<Tabs.Tab value="risk" leftSection={<ShieldAlert size={14} />}>
|
<Tabs.Tab value="risk" leftSection={<ShieldAlert size={14} />}>
|
||||||
Risk assignment
|
Risk assignment
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
) : null}
|
) : null}
|
||||||
{showOpsTabs && bookingId ? (
|
{showOpsTabs && canOps && bookingId ? (
|
||||||
<Tabs.Tab value="incidents" leftSection={<AlertTriangle size={14} />}>
|
<Tabs.Tab value="incidents" leftSection={<AlertTriangle size={14} />}>
|
||||||
Incidents
|
Incidents
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
@@ -129,7 +131,7 @@ export function ClearanceOpsTabs({
|
|||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showOpsTabs && riskMs && bookingId ? (
|
{showOpsTabs && canOps && riskMs && bookingId ? (
|
||||||
<Tabs.Panel value="risk">
|
<Tabs.Panel value="risk">
|
||||||
<SectionCard icon={ShieldAlert} title="Customs risk" accent="edr-green">
|
<SectionCard icon={ShieldAlert} title="Customs risk" accent="edr-green">
|
||||||
<AssignRiskCard bookingId={bookingId} milestone={riskMs} />
|
<AssignRiskCard bookingId={bookingId} milestone={riskMs} />
|
||||||
@@ -137,7 +139,7 @@ export function ClearanceOpsTabs({
|
|||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showOpsTabs && bookingId ? (
|
{showOpsTabs && canOps && bookingId ? (
|
||||||
<Tabs.Panel value="incidents">
|
<Tabs.Panel value="incidents">
|
||||||
<SectionCard icon={AlertTriangle} title="Incident reports" accent="edr-green">
|
<SectionCard icon={AlertTriangle} title="Incident reports" accent="edr-green">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import {
|
|||||||
fetchViewableFile,
|
fetchViewableFile,
|
||||||
} from "@/services/files.service";
|
} from "@/services/files.service";
|
||||||
import { useContractClearanceMutations } from "@/hooks/contracts/useContracts";
|
import { useContractClearanceMutations } from "@/hooks/contracts/useContracts";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||||
import { useFileViewer } from "@/hooks/useFileViewer";
|
import { useFileViewer } from "@/hooks/useFileViewer";
|
||||||
|
|
||||||
export interface ContractClearanceReviewSectionProps {
|
export interface ContractClearanceReviewSectionProps {
|
||||||
@@ -123,6 +125,21 @@ export function ContractClearanceReviewSection({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const { view, viewer } = useFileViewer();
|
const { view, viewer } = useFileViewer();
|
||||||
|
|
||||||
|
// Mirror the API guards: Path A (self-clearance) actions need
|
||||||
|
// ops_clearance_review; Path B (customs) review needs clearance_review or
|
||||||
|
// the ET phased key. Without the matching key every action would 403 — show
|
||||||
|
// the audit view instead of dead buttons.
|
||||||
|
const { user } = useAuth();
|
||||||
|
const canReviewHere = selfClear
|
||||||
|
? hasPermission(user, FREIGHT_PERMS.contracts.opsClearanceReview)
|
||||||
|
: hasPermission(user, FREIGHT_PERMS.contracts.clearanceReview) ||
|
||||||
|
hasPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions);
|
||||||
|
// Finalize has its own API key on the customs path (contracts:finalize_clearance).
|
||||||
|
const canFinalizeHere = selfClear
|
||||||
|
? hasPermission(user, FREIGHT_PERMS.contracts.opsClearanceReview)
|
||||||
|
: hasPermission(user, FREIGHT_PERMS.contracts.finalizeClearance);
|
||||||
|
readOnly = readOnly || !canReviewHere;
|
||||||
|
|
||||||
const reviewerTeam = selfClear ? "Operations" : "Global Logistics";
|
const reviewerTeam = selfClear ? "Operations" : "Global Logistics";
|
||||||
|
|
||||||
const { data: clearance, isLoading } = useQuery({
|
const { data: clearance, isLoading } = useQuery({
|
||||||
@@ -484,7 +501,7 @@ export function ContractClearanceReviewSection({
|
|||||||
color="edr-green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<CheckCircle2 size={16} />}
|
leftSection={<CheckCircle2 size={16} />}
|
||||||
disabled={!clearance.allApproved}
|
disabled={!clearance.allApproved || !canFinalizeHere}
|
||||||
loading={finalizeClearance.isPending}
|
loading={finalizeClearance.isPending}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
finalizeClearance.mutate(undefined, {
|
finalizeClearance.mutate(undefined, {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
|||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import type { TrainScheduleDetail } from "@/types/trainScheduling";
|
import type { TrainScheduleDetail } from "@/types/trainScheduling";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||||
|
|
||||||
interface ScheduleBatchPanelProps {
|
interface ScheduleBatchPanelProps {
|
||||||
schedule: TrainScheduleDetail;
|
schedule: TrainScheduleDetail;
|
||||||
@@ -31,6 +33,8 @@ const windowColor: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) {
|
export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const canMarkPaid = hasPermission(user, FREIGHT_PERMS.trainScheduling.markPaid);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const actions = {
|
const actions = {
|
||||||
runBatch: useMutation(api.trainScheduling.runBatch.mutationOptions()),
|
runBatch: useMutation(api.trainScheduling.runBatch.mutationOptions()),
|
||||||
@@ -179,7 +183,7 @@ export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) {
|
|||||||
<Table.Td>
|
<Table.Td>
|
||||||
{!locked && (
|
{!locked && (
|
||||||
<Group gap={6} justify="flex-end" wrap="nowrap">
|
<Group gap={6} justify="flex-end" wrap="nowrap">
|
||||||
{b.status !== "PAID" && (
|
{canMarkPaid && b.status !== "PAID" && (
|
||||||
<Button
|
<Button
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|||||||
@@ -6,7 +6,32 @@ export const FREIGHT_PERMS = {
|
|||||||
view: "edr_freight_app:overview:view",
|
view: "edr_freight_app:overview:view",
|
||||||
},
|
},
|
||||||
support: {
|
support: {
|
||||||
view: "edr_freight_app:support:view",
|
agentView: "edr_freight_app:support:agent_view",
|
||||||
|
agentSend: "edr_freight_app:support:agent_send",
|
||||||
|
},
|
||||||
|
reports: {
|
||||||
|
view: "edr_freight_app:reports:view",
|
||||||
|
},
|
||||||
|
procurement: {
|
||||||
|
view: "edr_freight_app:procurement:view",
|
||||||
|
vendorManage: "edr_freight_app:procurement:vendor_manage",
|
||||||
|
acquisitionManage: "edr_freight_app:procurement:acquisition_manage",
|
||||||
|
disposalManage: "edr_freight_app:procurement:disposal_manage",
|
||||||
|
},
|
||||||
|
compliance: {
|
||||||
|
view: "edr_freight_app:compliance:view",
|
||||||
|
manage: "edr_freight_app:compliance:manage",
|
||||||
|
},
|
||||||
|
facilities: {
|
||||||
|
view: "edr_freight_app:facilities:view",
|
||||||
|
manage: "edr_freight_app:facilities:manage",
|
||||||
|
},
|
||||||
|
tradeAccess: {
|
||||||
|
view: "edr_freight_app:trade_access:view",
|
||||||
|
manage: "edr_freight_app:trade_access:manage",
|
||||||
|
},
|
||||||
|
staffUsers: {
|
||||||
|
view: "edr_freight_app:staff:users:view",
|
||||||
},
|
},
|
||||||
bookings: {
|
bookings: {
|
||||||
view: "edr_freight_app:bookings:view",
|
view: "edr_freight_app:bookings:view",
|
||||||
@@ -27,6 +52,7 @@ export const FREIGHT_PERMS = {
|
|||||||
uploadClearanceOutput: "edr_freight_app:bookings:upload_clearance_output",
|
uploadClearanceOutput: "edr_freight_app:bookings:upload_clearance_output",
|
||||||
finalizeClearance: "edr_freight_app:bookings:finalize_clearance",
|
finalizeClearance: "edr_freight_app:bookings:finalize_clearance",
|
||||||
docReviewAlert: "edr_freight_app:bookings:doc_review_alert",
|
docReviewAlert: "edr_freight_app:bookings:doc_review_alert",
|
||||||
|
governmentExpedite: "edr_freight_app:bookings:government_expedite",
|
||||||
},
|
},
|
||||||
contracts: {
|
contracts: {
|
||||||
view: "edr_freight_app:contracts:view",
|
view: "edr_freight_app:contracts:view",
|
||||||
@@ -61,6 +87,9 @@ export const FREIGHT_PERMS = {
|
|||||||
clearanceEtActions: "edr_freight_app:contracts:clearance_et_actions",
|
clearanceEtActions: "edr_freight_app:contracts:clearance_et_actions",
|
||||||
clearanceDjActions: "edr_freight_app:contracts:clearance_dj_actions",
|
clearanceDjActions: "edr_freight_app:contracts:clearance_dj_actions",
|
||||||
suspend: "edr_freight_app:contracts:suspend",
|
suspend: "edr_freight_app:contracts:suspend",
|
||||||
|
editDocument: "edr_freight_app:contracts:edit_document",
|
||||||
|
finalInvoiceRaise: "edr_freight_app:contracts:final_invoice_raise",
|
||||||
|
finalInvoiceConfirm: "edr_freight_app:contracts:final_invoice_confirm",
|
||||||
},
|
},
|
||||||
trainScheduling: {
|
trainScheduling: {
|
||||||
view: "edr_freight_app:train_scheduling:view",
|
view: "edr_freight_app:train_scheduling:view",
|
||||||
@@ -69,6 +98,9 @@ export const FREIGHT_PERMS = {
|
|||||||
cancel: "edr_freight_app:train_scheduling:cancel",
|
cancel: "edr_freight_app:train_scheduling:cancel",
|
||||||
reschedule: "edr_freight_app:train_scheduling:reschedule",
|
reschedule: "edr_freight_app:train_scheduling:reschedule",
|
||||||
rulesManage: "edr_freight_app:train_scheduling:rules_manage",
|
rulesManage: "edr_freight_app:train_scheduling:rules_manage",
|
||||||
|
dispatch: "edr_freight_app:train_scheduling:dispatch",
|
||||||
|
markPaid: "edr_freight_app:train_scheduling:mark_paid",
|
||||||
|
expireBooking: "edr_freight_app:train_scheduling:expire_booking",
|
||||||
},
|
},
|
||||||
fleet: {
|
fleet: {
|
||||||
view: "edr_freight_app:fleet:view",
|
view: "edr_freight_app:fleet:view",
|
||||||
@@ -88,13 +120,9 @@ export const FREIGHT_PERMS = {
|
|||||||
},
|
},
|
||||||
payments: {
|
payments: {
|
||||||
view: "edr_freight_app:payments:view",
|
view: "edr_freight_app:payments:view",
|
||||||
verify: "edr_freight_app:payments:verify",
|
|
||||||
refund: "edr_freight_app:payments:refund",
|
|
||||||
},
|
},
|
||||||
invoices: {
|
invoices: {
|
||||||
view: "edr_freight_app:invoices:view",
|
view: "edr_freight_app:invoices:view",
|
||||||
create: "edr_freight_app:invoices:create",
|
|
||||||
cancel: "edr_freight_app:invoices:cancel",
|
|
||||||
export: "edr_freight_app:invoices:export",
|
export: "edr_freight_app:invoices:export",
|
||||||
},
|
},
|
||||||
firstMile: {
|
firstMile: {
|
||||||
@@ -191,14 +219,12 @@ export const FREIGHT_PERMS = {
|
|||||||
create: "edr_freight_app:fuel:create",
|
create: "edr_freight_app:fuel:create",
|
||||||
update: "edr_freight_app:fuel:update",
|
update: "edr_freight_app:fuel:update",
|
||||||
delete: "edr_freight_app:fuel:delete",
|
delete: "edr_freight_app:fuel:delete",
|
||||||
approve: "edr_freight_app:fuel:approve",
|
|
||||||
},
|
},
|
||||||
maintenance: {
|
maintenance: {
|
||||||
view: "edr_freight_app:maintenance:view",
|
view: "edr_freight_app:maintenance:view",
|
||||||
create: "edr_freight_app:maintenance:create",
|
create: "edr_freight_app:maintenance:create",
|
||||||
update: "edr_freight_app:maintenance:update",
|
update: "edr_freight_app:maintenance:update",
|
||||||
delete: "edr_freight_app:maintenance:delete",
|
delete: "edr_freight_app:maintenance:delete",
|
||||||
complete: "edr_freight_app:maintenance:complete",
|
|
||||||
},
|
},
|
||||||
fleetReports: {
|
fleetReports: {
|
||||||
view: "edr_freight_app:fleet_reports:view",
|
view: "edr_freight_app:fleet_reports:view",
|
||||||
@@ -278,6 +304,14 @@ export const FREIGHT_PERMS = {
|
|||||||
view: "edr_freight_app:settings:dropdown:view",
|
view: "edr_freight_app:settings:dropdown:view",
|
||||||
manage: "edr_freight_app:settings:dropdown:manage",
|
manage: "edr_freight_app:settings:dropdown:manage",
|
||||||
},
|
},
|
||||||
|
exchangeRate: {
|
||||||
|
view: "edr_freight_app:settings:exchange_rate:view",
|
||||||
|
manage: "edr_freight_app:settings:exchange_rate:manage",
|
||||||
|
},
|
||||||
|
contractTemplates: {
|
||||||
|
view: "edr_freight_app:settings:contract_templates:view",
|
||||||
|
manage: "edr_freight_app:settings:contract_templates:manage",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
audit: {
|
audit: {
|
||||||
view: "edr_freight_app:audit:view",
|
view: "edr_freight_app:audit:view",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ArrowLeft, Download } from "lucide-react";
|
import { ArrowLeft, Download } from "lucide-react";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
@@ -60,6 +62,8 @@ function InfoField({ label, value }: { label: string; value?: string | null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function InvoiceDetailPage() {
|
export default function InvoiceDetailPage() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const canExport = hasPermission(user, FREIGHT_PERMS.invoices.export);
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
@@ -124,6 +128,7 @@ export default function InvoiceDetailPage() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
aria-label="Download invoice"
|
aria-label="Download invoice"
|
||||||
|
disabled={!canExport}
|
||||||
loading={downloading}
|
loading={downloading}
|
||||||
onClick={() => void downloadDocument()}
|
onClick={() => void downloadDocument()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import {
|
|||||||
} from "@/features/support/useSupport";
|
} from "@/features/support/useSupport";
|
||||||
import { useSupportSocket } from "@/features/support/useSupportSocket";
|
import { useSupportSocket } from "@/features/support/useSupportSocket";
|
||||||
import { customersService } from "@/services/customers.service";
|
import { customersService } from "@/services/customers.service";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||||
|
|
||||||
type ReadFilter = "ALL" | "UNREAD";
|
type ReadFilter = "ALL" | "UNREAD";
|
||||||
|
|
||||||
@@ -401,6 +403,8 @@ function ConversationThread({
|
|||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = useMessages(conversation.id);
|
} = useMessages(conversation.id);
|
||||||
const send = useSendMessage(conversation.id);
|
const send = useSendMessage(conversation.id);
|
||||||
|
const { user: agentUser } = useAuth();
|
||||||
|
const canSend = hasPermission(agentUser, FREIGHT_PERMS.support.agentSend);
|
||||||
const markRead = useMarkConversationRead();
|
const markRead = useMarkConversationRead();
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
@@ -656,7 +660,7 @@ function ConversationThread({
|
|||||||
color="edr-green"
|
color="edr-green"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
loading={send.isPending}
|
loading={send.isPending}
|
||||||
disabled={!draft.trim() && attach.attachments.length === 0}
|
disabled={!canSend || (!draft.trim() && attach.attachments.length === 0)}
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
<Send size={18} />
|
<Send size={18} />
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { KpiStrip, PageContainer } from "@/components/page";
|
import { KpiStrip, PageContainer } from "@/components/page";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||||
import {
|
import {
|
||||||
autoFillPlacements,
|
autoFillPlacements,
|
||||||
mergePlacementsWithSaved,
|
mergePlacementsWithSaved,
|
||||||
@@ -102,6 +104,7 @@ const parseError = (error: unknown, fallback: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function TrainScheduleV2DetailPage() {
|
export default function TrainScheduleV2DetailPage() {
|
||||||
|
const { user: authUser } = useAuth();
|
||||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
@@ -394,7 +397,9 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const canEditBookings = ["DRAFT", "SCHEDULED"].includes(schedule.status);
|
const canEditBookings = ["DRAFT", "SCHEDULED"].includes(schedule.status);
|
||||||
const canDispatch = schedule.status === "SCHEDULED";
|
const canDispatch =
|
||||||
|
schedule.status === "SCHEDULED" &&
|
||||||
|
hasPermission(authUser, FREIGHT_PERMS.trainScheduling.dispatch);
|
||||||
|
|
||||||
// Dispatch readiness: bookings with no wagon, and wagon-loaded bookings whose
|
// Dispatch readiness: bookings with no wagon, and wagon-loaded bookings whose
|
||||||
// cargo staff never marked loaded. Both are warnings, not blockers — staff can
|
// cargo staff never marked loaded. Both are warnings, not blockers — staff can
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { api } from "@/services/api";
|
|||||||
import { formatRouteLabel } from "@/services/routes.service";
|
import { formatRouteLabel } from "@/services/routes.service";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAuth } from "@/auth/useAuth";
|
import { useAuth } from "@/auth/useAuth";
|
||||||
import { canCreateSchedule, canUpdateSchedule } from "@/lib/permissions";
|
import { FREIGHT_PERMS, canCreateSchedule, hasPermission } from "@/lib/permissions";
|
||||||
import type {
|
import type {
|
||||||
CreateScheduleWindowRulePayload,
|
CreateScheduleWindowRulePayload,
|
||||||
FreightType,
|
FreightType,
|
||||||
@@ -111,7 +111,7 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const canCreate = canCreateSchedule(user);
|
const canCreate = canCreateSchedule(user);
|
||||||
const canUpdate = canUpdateSchedule(user);
|
const canDispatch = hasPermission(user, FREIGHT_PERMS.trainScheduling.dispatch);
|
||||||
const { viewMode, setViewMode } = useFleetViewMode("train-scheduling-v2");
|
const { viewMode, setViewMode } = useFleetViewMode("train-scheduling-v2");
|
||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -491,7 +491,7 @@ export default function TrainScheduleV2ListPage() {
|
|||||||
{/* Start the run. Same transition as the detail page's
|
{/* Start the run. Same transition as the detail page's
|
||||||
Dispatch button — that page also shows unassigned-wagon
|
Dispatch button — that page also shows unassigned-wagon
|
||||||
and not-loaded warnings, so it stays the fuller surface. */}
|
and not-loaded warnings, so it stays the fuller surface. */}
|
||||||
{canUpdate && schedule.status === "SCHEDULED" ? (
|
{canDispatch && schedule.status === "SCHEDULED" ? (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<Play size={15} />}
|
leftSection={<Play size={15} />}
|
||||||
onClick={() => setDispatchTarget(schedule)}
|
onClick={() => setDispatchTarget(schedule)}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import {
|
|||||||
import { openPdfBlob } from '@/components/warehouses/pdf';
|
import { openPdfBlob } from '@/components/warehouses/pdf';
|
||||||
import { buildWarehouseExitPaperPdf } from '@/components/warehouses/warehousePdf';
|
import { buildWarehouseExitPaperPdf } from '@/components/warehouses/warehousePdf';
|
||||||
import { extractErrorMessage } from '@/components/warehouses/options';
|
import { extractErrorMessage } from '@/components/warehouses/options';
|
||||||
|
import { useAuth } from '@/auth/useAuth';
|
||||||
|
import { FREIGHT_PERMS, hasPermission } from '@/lib/permissions';
|
||||||
|
|
||||||
const STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
const STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
||||||
DRAFT: 'gray',
|
DRAFT: 'gray',
|
||||||
@@ -171,6 +173,9 @@ export default function WarehouseInvoicesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () => void }) {
|
function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () => void }) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const mayRecordPayment = hasPermission(user, FREIGHT_PERMS.warehouseFeeInvoices.pay);
|
||||||
|
const canCancelInvoice = hasPermission(user, FREIGHT_PERMS.warehouseFeeInvoices.cancel);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: inv, isLoading } = useQuery(
|
const { data: inv, isLoading } = useQuery(
|
||||||
@@ -430,9 +435,11 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
|||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<Button leftSection={<CreditCard size={16} />} loading={payOnline.isPending} onClick={handleOnlinePay}>
|
{mayRecordPayment && (
|
||||||
Pay with {gatewayMethod === 'WAAFI' ? 'Waafi' : 'Telebirr'}
|
<Button leftSection={<CreditCard size={16} />} loading={payOnline.isPending} onClick={handleOnlinePay}>
|
||||||
</Button>
|
Pay with {gatewayMethod === 'WAAFI' ? 'Waafi' : 'Telebirr'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Divider label="Record manual payment" labelPosition="left" />
|
<Divider label="Record manual payment" labelPosition="left" />
|
||||||
@@ -456,9 +463,11 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
|||||||
onChange={(e) => setDriverPhone(e.currentTarget.value)}
|
onChange={(e) => setDriverPhone(e.currentTarget.value)}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<Button leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
|
{mayRecordPayment && (
|
||||||
Pay
|
<Button leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
|
||||||
</Button>
|
Pay
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -506,7 +515,7 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
|||||||
Gate clearance & exit paper
|
Gate clearance & exit paper
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{inv.status !== 'PAID' && inv.status !== 'CANCELLED' && (
|
{inv.status !== 'PAID' && inv.status !== 'CANCELLED' && canCancelInvoice && (
|
||||||
<Button variant="light" color="red" leftSection={<Ban size={16} />} loading={cancel.isPending} onClick={handleCancel}>
|
<Button variant="light" color="red" leftSection={<Ban size={16} />} loading={cancel.isPending} onClick={handleCancel}>
|
||||||
Cancel invoice
|
Cancel invoice
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user