diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx
index bda88d912..4c5016e94 100644
--- a/apps/edr-freight-web/backoffice/src/App.tsx
+++ b/apps/edr-freight-web/backoffice/src/App.tsx
@@ -164,7 +164,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
label: "Reports",
href: "/dashboard/reports",
icon: ,
- permission: FREIGHT_PERMS.bookings.view,
+ permission: FREIGHT_PERMS.reports.view,
},
{
label: "Customers",
@@ -196,19 +196,19 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
label: "Payments",
href: "/dashboard/payments",
icon: ,
- permission: FREIGHT_PERMS.bookings.view,
+ permission: FREIGHT_PERMS.payments.view,
},
{
label: "Invoices",
href: "/dashboard/invoices",
icon: ,
- permission: FREIGHT_PERMS.bookings.view,
+ permission: FREIGHT_PERMS.invoices.view,
},
{
label: "Support",
href: "/dashboard/support",
icon: ,
- permission: FREIGHT_PERMS.support.view,
+ permission: FREIGHT_PERMS.support.agentView,
},
...demoItems,
],
@@ -844,26 +844,30 @@ const App = () => {
element={}
/>
}>
- } />
- } />
- } />
+ } />
+ } />
+ } />
{/* Dev/testing page for the mock AI booking assistant. */}
}
+ element={
+
+
+
+ }
/>
} />
- } />
+ } />
+
}
/>
- } />
+ } />
{
+
}
@@ -891,19 +895,27 @@ const App = () => {
+
}
/>
- } />
+ } />
}
+ element={
+
+
+
+ }
/>
}
+ element={
+
+
+
+ }
/>
{/* Legacy booking-based clearance URLs → the contract clearance hub. */}
{
path="bookings/:id/milestones"
element={}
/>
- } />
- } />
- }
- />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- }
- />
- } />
- } />
- } />
- }
- />
- }
- />
- } />
- } />
- }
- />
- }
- />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
{
}
/>
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
{
}
/>
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
{/* Legacy embedded user management routes */}
{/* } />
diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceOpsTabs.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceOpsTabs.tsx
index 610fb7e3d..a61646d6f 100644
--- a/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceOpsTabs.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceOpsTabs.tsx
@@ -68,6 +68,8 @@ export function ClearanceOpsTabs({
Boolean(exchangeEntityId) &&
(hasPermission(user, FREIGHT_PERMS.contracts.clearanceEtActions) ||
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;
if (!hasTabs) {
@@ -98,12 +100,12 @@ export function ClearanceOpsTabs({
Document exchange
) : null}
- {showOpsTabs && riskMs ? (
+ {showOpsTabs && canOps && riskMs ? (
}>
Risk assignment
) : null}
- {showOpsTabs && bookingId ? (
+ {showOpsTabs && canOps && bookingId ? (
}>
Incidents
@@ -129,7 +131,7 @@ export function ClearanceOpsTabs({
) : null}
- {showOpsTabs && riskMs && bookingId ? (
+ {showOpsTabs && canOps && riskMs && bookingId ? (
@@ -137,7 +139,7 @@ export function ClearanceOpsTabs({
) : null}
- {showOpsTabs && bookingId ? (
+ {showOpsTabs && canOps && bookingId ? (
diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx
index c773933ea..5e2ca9e83 100644
--- a/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/contracts/ContractClearanceReviewSection.tsx
@@ -41,6 +41,8 @@ import {
fetchViewableFile,
} from "@/services/files.service";
import { useContractClearanceMutations } from "@/hooks/contracts/useContracts";
+import { useAuth } from "@/auth/useAuth";
+import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import { useFileViewer } from "@/hooks/useFileViewer";
export interface ContractClearanceReviewSectionProps {
@@ -123,6 +125,21 @@ export function ContractClearanceReviewSection({
} | null>(null);
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 { data: clearance, isLoading } = useQuery({
@@ -484,7 +501,7 @@ export function ContractClearanceReviewSection({
color="edr-green"
radius="md"
leftSection={}
- disabled={!clearance.allApproved}
+ disabled={!clearance.allApproved || !canFinalizeHere}
loading={finalizeClearance.isPending}
onClick={() =>
finalizeClearance.mutate(undefined, {
diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx
index d44ef4540..9b410ce88 100644
--- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/ScheduleBatchPanel.tsx
@@ -19,6 +19,8 @@ import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
import { api } from "@/services/api";
import { useToast } from "@/hooks/use-toast";
import type { TrainScheduleDetail } from "@/types/trainScheduling";
+import { useAuth } from "@/auth/useAuth";
+import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
interface ScheduleBatchPanelProps {
schedule: TrainScheduleDetail;
@@ -31,6 +33,8 @@ const windowColor: Record = {
};
export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) {
+ const { user } = useAuth();
+ const canMarkPaid = hasPermission(user, FREIGHT_PERMS.trainScheduling.markPaid);
const { toast } = useToast();
const actions = {
runBatch: useMutation(api.trainScheduling.runBatch.mutationOptions()),
@@ -179,7 +183,7 @@ export function ScheduleBatchPanel({ schedule }: ScheduleBatchPanelProps) {
{!locked && (
- {b.status !== "PAID" && (
+ {canMarkPaid && b.status !== "PAID" && (
+ {mayRecordPayment && (
+ } loading={payOnline.isPending} onClick={handleOnlinePay}>
+ Pay with {gatewayMethod === 'WAAFI' ? 'Waafi' : 'Telebirr'}
+
+ )}
@@ -456,9 +463,11 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
onChange={(e) => setDriverPhone(e.currentTarget.value)}
style={{ flex: 1 }}
/>
- } loading={pay.isPending} onClick={handlePay}>
- Pay
-
+ {mayRecordPayment && (
+ } loading={pay.isPending} onClick={handlePay}>
+ Pay
+
+ )}
>
)}
@@ -506,7 +515,7 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
Gate clearance & exit paper
)}
- {inv.status !== 'PAID' && inv.status !== 'CANCELLED' && (
+ {inv.status !== 'PAID' && inv.status !== 'CANCELLED' && canCancelInvoice && (
} loading={cancel.isPending} onClick={handleCancel}>
Cancel invoice