mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
merge
This commit is contained in:
@@ -166,7 +166,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Reports",
|
||||
href: "/dashboard/reports",
|
||||
icon: <BarChart3 />,
|
||||
permission: FREIGHT_PERMS.bookings.view,
|
||||
permission: FREIGHT_PERMS.reports.view,
|
||||
},
|
||||
{
|
||||
label: "Customers",
|
||||
@@ -204,19 +204,19 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Payments",
|
||||
href: "/dashboard/payments",
|
||||
icon: <Wallet />,
|
||||
permission: FREIGHT_PERMS.bookings.view,
|
||||
permission: FREIGHT_PERMS.payments.view,
|
||||
},
|
||||
{
|
||||
label: "Invoices",
|
||||
href: "/dashboard/invoices",
|
||||
icon: <Receipt />,
|
||||
permission: FREIGHT_PERMS.bookings.view,
|
||||
permission: FREIGHT_PERMS.invoices.view,
|
||||
},
|
||||
{
|
||||
label: "Support",
|
||||
href: "/dashboard/support",
|
||||
icon: <LifeBuoy />,
|
||||
permission: FREIGHT_PERMS.support.view,
|
||||
permission: FREIGHT_PERMS.support.agentView,
|
||||
},
|
||||
...demoItems,
|
||||
],
|
||||
@@ -852,26 +852,30 @@ const App = () => {
|
||||
element={<Navigate to="/dashboard/overview" replace />}
|
||||
/>
|
||||
<Route path="/dashboard" element={<DashboardShell />}>
|
||||
<Route path="overview" element={<OverviewPage />} />
|
||||
<Route path="reports" element={<ReportsHubPage />} />
|
||||
<Route path="reports/:reportKey" element={<ReportPage />} />
|
||||
<Route path="overview" element={<RequirePermission permission={FREIGHT_PERMS.overview.view}><OverviewPage /></RequirePermission>} />
|
||||
<Route path="reports" element={<RequirePermission permission={FREIGHT_PERMS.reports.view}><ReportsHubPage /></RequirePermission>} />
|
||||
<Route path="reports/:reportKey" element={<RequirePermission permission={FREIGHT_PERMS.reports.view}><ReportPage /></RequirePermission>} />
|
||||
{/* Dev/testing page for the mock AI booking assistant. */}
|
||||
<Route
|
||||
path="ai-booking-mock-test"
|
||||
element={<AiBookingMockTestPage />}
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||
<AiBookingMockTestPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<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
|
||||
path="payments"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||
<RequirePermission permission={FREIGHT_PERMS.payments.view}>
|
||||
<PaymentsPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<Route path="support" element={<SupportInboxPage />} />
|
||||
<Route path="support" element={<RequirePermission permission={FREIGHT_PERMS.support.agentView}><SupportInboxPage /></RequirePermission>} />
|
||||
<Route
|
||||
path="customers"
|
||||
element={
|
||||
@@ -891,7 +895,7 @@ const App = () => {
|
||||
<Route
|
||||
path="invoices"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||
<RequirePermission permission={FREIGHT_PERMS.invoices.view}>
|
||||
<InvoicesPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
@@ -899,12 +903,12 @@ const App = () => {
|
||||
<Route
|
||||
path="invoices/:id"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||
<RequirePermission permission={FREIGHT_PERMS.invoices.view}>
|
||||
<InvoiceDetailPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<Route path="booking-requests/new" element={<NewBookingPage />} />
|
||||
<Route path="booking-requests/new" element={<RequirePermission permission={FREIGHT_PERMS.bookings.view}><NewBookingPage /></RequirePermission>} />
|
||||
<Route
|
||||
path="wagon-cancellations"
|
||||
element={
|
||||
@@ -917,11 +921,19 @@ const App = () => {
|
||||
/>
|
||||
<Route
|
||||
path="booking-requests/:id"
|
||||
element={<BookingRequestDetailPage />}
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.bookings.view}>
|
||||
<BookingRequestDetailPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
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. */}
|
||||
<Route
|
||||
@@ -1109,44 +1121,26 @@ const App = () => {
|
||||
path="bookings/:id/milestones"
|
||||
element={<BookingMilestonesRedirect />}
|
||||
/>
|
||||
<Route path="warehouses" element={<WarehouseListPage />} />
|
||||
<Route path="warehouses/:id" element={<WarehouseDetailPage />} />
|
||||
<Route
|
||||
path="warehouse-inventory"
|
||||
element={<WarehouseInventoryPage />}
|
||||
/>
|
||||
<Route path="import-warehouse" element={<ImportWarehouseFlowPage />} />
|
||||
<Route path="export-warehouse" element={<ExportWarehouseFlowPage />} />
|
||||
<Route path="arrival-queue" element={<ArrivalQueuePage />} />
|
||||
<Route path="loading-queue" element={<LoadingQueuePage />} />
|
||||
<Route path="intercity" element={<IntercityPage />} />
|
||||
<Route path="trucks-on-site" element={<TrucksOnSitePage />} />
|
||||
<Route path="import-trucks" element={<ImportTrucksPage />} />
|
||||
<Route
|
||||
path="edr-last-mile-returns"
|
||||
element={<EDRLastMileReturnsPage />}
|
||||
/>
|
||||
<Route path="container-returns" element={<ContainerReturnsPage />} />
|
||||
<Route path="loaded-inventory" element={<LoadedInventoryPage />} />
|
||||
<Route path="dispatch-queue" element={<DispatchQueuePage />} />
|
||||
<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 path="warehouses" element={<RequirePermission permission={FREIGHT_PERMS.warehouses.view}><WarehouseListPage /></RequirePermission>} />
|
||||
<Route path="warehouses/:id" element={<RequirePermission permission={FREIGHT_PERMS.warehouses.view}><WarehouseDetailPage /></RequirePermission>} />
|
||||
<Route path="warehouse-inventory" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><WarehouseInventoryPage /></RequirePermission>} />
|
||||
<Route path="import-warehouse" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ImportWarehouseFlowPage /></RequirePermission>} />
|
||||
<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="loading-queue" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><LoadingQueuePage /></RequirePermission>} />
|
||||
<Route path="intercity" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><IntercityPage /></RequirePermission>} />
|
||||
<Route path="trucks-on-site" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><TrucksOnSitePage /></RequirePermission>} />
|
||||
<Route path="import-trucks" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ImportTrucksPage /></RequirePermission>} />
|
||||
<Route path="edr-last-mile-returns" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><EDRLastMileReturnsPage /></RequirePermission>} />
|
||||
<Route path="container-returns" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ContainerReturnsPage /></RequirePermission>} />
|
||||
<Route path="loaded-inventory" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><LoadedInventoryPage /></RequirePermission>} />
|
||||
<Route path="dispatch-queue" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><DispatchQueuePage /></RequirePermission>} />
|
||||
<Route path="export-djibouti-unloading" element={<RequirePermission permission={FREIGHT_PERMS.warehouseInventory.view}><ExportDjiboutiUnloadingQueuePage /></RequirePermission>} />
|
||||
<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="warehouse-rules" element={<RequirePermission permission={[FREIGHT_PERMS.warehouseAllocationRules.view, FREIGHT_PERMS.warehouseFeeRules.view]}><WarehouseRulesPage /></RequirePermission>} />
|
||||
<Route path="warehouse-fee-invoices" element={<RequirePermission permission={FREIGHT_PERMS.warehouseFeeInvoices.view}><WarehouseInvoicesPage /></RequirePermission>} />
|
||||
<Route path="warehouse-dashboard" element={<RequirePermission permission={FREIGHT_PERMS.warehouseDashboard.view}><WarehouseDashboardPage /></RequirePermission>} />
|
||||
|
||||
<Route
|
||||
path="operations/train-scheduling"
|
||||
@@ -1354,62 +1348,6 @@ const App = () => {
|
||||
</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
|
||||
path="fuel-purchases"
|
||||
element={
|
||||
@@ -1490,108 +1428,6 @@ const App = () => {
|
||||
</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 */}
|
||||
{/* <Route path="user-management" element={<UserManagementPage />} />
|
||||
|
||||
@@ -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
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{showOpsTabs && riskMs ? (
|
||||
{showOpsTabs && canOps && riskMs ? (
|
||||
<Tabs.Tab value="risk" leftSection={<ShieldAlert size={14} />}>
|
||||
Risk assignment
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{showOpsTabs && bookingId ? (
|
||||
{showOpsTabs && canOps && bookingId ? (
|
||||
<Tabs.Tab value="incidents" leftSection={<AlertTriangle size={14} />}>
|
||||
Incidents
|
||||
</Tabs.Tab>
|
||||
@@ -129,7 +131,7 @@ export function ClearanceOpsTabs({
|
||||
</Tabs.Panel>
|
||||
) : null}
|
||||
|
||||
{showOpsTabs && riskMs && bookingId ? (
|
||||
{showOpsTabs && canOps && riskMs && bookingId ? (
|
||||
<Tabs.Panel value="risk">
|
||||
<SectionCard icon={ShieldAlert} title="Customs risk" accent="edr-green">
|
||||
<AssignRiskCard bookingId={bookingId} milestone={riskMs} />
|
||||
@@ -137,7 +139,7 @@ export function ClearanceOpsTabs({
|
||||
</Tabs.Panel>
|
||||
) : null}
|
||||
|
||||
{showOpsTabs && bookingId ? (
|
||||
{showOpsTabs && canOps && bookingId ? (
|
||||
<Tabs.Panel value="incidents">
|
||||
<SectionCard icon={AlertTriangle} title="Incident reports" accent="edr-green">
|
||||
<Stack gap="sm">
|
||||
|
||||
@@ -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={<CheckCircle2 size={16} />}
|
||||
disabled={!clearance.allApproved}
|
||||
disabled={!clearance.allApproved || !canFinalizeHere}
|
||||
loading={finalizeClearance.isPending}
|
||||
onClick={() =>
|
||||
finalizeClearance.mutate(undefined, {
|
||||
|
||||
@@ -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<string, string> = {
|
||||
};
|
||||
|
||||
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) {
|
||||
<Table.Td>
|
||||
{!locked && (
|
||||
<Group gap={6} justify="flex-end" wrap="nowrap">
|
||||
{b.status !== "PAID" && (
|
||||
{canMarkPaid && b.status !== "PAID" && (
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="light"
|
||||
|
||||
@@ -6,7 +6,32 @@ export const FREIGHT_PERMS = {
|
||||
view: "edr_freight_app:overview:view",
|
||||
},
|
||||
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: {
|
||||
view: "edr_freight_app:bookings:view",
|
||||
@@ -27,6 +52,7 @@ export const FREIGHT_PERMS = {
|
||||
uploadClearanceOutput: "edr_freight_app:bookings:upload_clearance_output",
|
||||
finalizeClearance: "edr_freight_app:bookings:finalize_clearance",
|
||||
docReviewAlert: "edr_freight_app:bookings:doc_review_alert",
|
||||
governmentExpedite: "edr_freight_app:bookings:government_expedite",
|
||||
wagonCancellationView: "edr_freight_app:bookings:wagon_cancellation_view",
|
||||
wagonCancellationVoid: "edr_freight_app:bookings:wagon_cancellation_void",
|
||||
wagonCancellationRebook:
|
||||
@@ -65,6 +91,9 @@ export const FREIGHT_PERMS = {
|
||||
clearanceEtActions: "edr_freight_app:contracts:clearance_et_actions",
|
||||
clearanceDjActions: "edr_freight_app:contracts:clearance_dj_actions",
|
||||
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: {
|
||||
view: "edr_freight_app:train_scheduling:view",
|
||||
@@ -73,6 +102,9 @@ export const FREIGHT_PERMS = {
|
||||
cancel: "edr_freight_app:train_scheduling:cancel",
|
||||
reschedule: "edr_freight_app:train_scheduling:reschedule",
|
||||
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: {
|
||||
view: "edr_freight_app:fleet:view",
|
||||
@@ -92,13 +124,9 @@ export const FREIGHT_PERMS = {
|
||||
},
|
||||
payments: {
|
||||
view: "edr_freight_app:payments:view",
|
||||
verify: "edr_freight_app:payments:verify",
|
||||
refund: "edr_freight_app:payments:refund",
|
||||
},
|
||||
invoices: {
|
||||
view: "edr_freight_app:invoices:view",
|
||||
create: "edr_freight_app:invoices:create",
|
||||
cancel: "edr_freight_app:invoices:cancel",
|
||||
export: "edr_freight_app:invoices:export",
|
||||
},
|
||||
firstMile: {
|
||||
@@ -195,14 +223,12 @@ export const FREIGHT_PERMS = {
|
||||
create: "edr_freight_app:fuel:create",
|
||||
update: "edr_freight_app:fuel:update",
|
||||
delete: "edr_freight_app:fuel:delete",
|
||||
approve: "edr_freight_app:fuel:approve",
|
||||
},
|
||||
maintenance: {
|
||||
view: "edr_freight_app:maintenance:view",
|
||||
create: "edr_freight_app:maintenance:create",
|
||||
update: "edr_freight_app:maintenance:update",
|
||||
delete: "edr_freight_app:maintenance:delete",
|
||||
complete: "edr_freight_app:maintenance:complete",
|
||||
},
|
||||
fleetReports: {
|
||||
view: "edr_freight_app:fleet_reports:view",
|
||||
@@ -282,6 +308,14 @@ export const FREIGHT_PERMS = {
|
||||
view: "edr_freight_app:settings:dropdown:view",
|
||||
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: {
|
||||
view: "edr_freight_app:audit:view",
|
||||
@@ -575,7 +609,9 @@ export function canViewScheduling(user: AuthUser | null | undefined): boolean {
|
||||
}
|
||||
|
||||
/** Any train-scheduling write action (create / update / cancel / reschedule). */
|
||||
export function canManageScheduling(user: AuthUser | null | undefined): boolean {
|
||||
export function canManageScheduling(
|
||||
user: AuthUser | null | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
hasPermission(user, FREIGHT_PERMS.trainScheduling.create) ||
|
||||
hasPermission(user, FREIGHT_PERMS.trainScheduling.update) ||
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowLeft, Download } from "lucide-react";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
@@ -60,6 +62,8 @@ function InfoField({ label, value }: { label: string; value?: string | null }) {
|
||||
}
|
||||
|
||||
export default function InvoiceDetailPage() {
|
||||
const { user } = useAuth();
|
||||
const canExport = hasPermission(user, FREIGHT_PERMS.invoices.export);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
@@ -124,6 +128,7 @@ export default function InvoiceDetailPage() {
|
||||
size="lg"
|
||||
radius="md"
|
||||
aria-label="Download invoice"
|
||||
disabled={!canExport}
|
||||
loading={downloading}
|
||||
onClick={() => void downloadDocument()}
|
||||
>
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
} from "@/features/support/useSupport";
|
||||
import { useSupportSocket } from "@/features/support/useSupportSocket";
|
||||
import { customersService } from "@/services/customers.service";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
|
||||
type ReadFilter = "ALL" | "UNREAD";
|
||||
|
||||
@@ -401,6 +403,8 @@ function ConversationThread({
|
||||
fetchNextPage,
|
||||
} = useMessages(conversation.id);
|
||||
const send = useSendMessage(conversation.id);
|
||||
const { user: agentUser } = useAuth();
|
||||
const canSend = hasPermission(agentUser, FREIGHT_PERMS.support.agentSend);
|
||||
const markRead = useMarkConversationRead();
|
||||
const [draft, setDraft] = useState("");
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@@ -656,7 +660,7 @@ function ConversationThread({
|
||||
color="edr-green"
|
||||
variant="filled"
|
||||
loading={send.isPending}
|
||||
disabled={!draft.trim() && attach.attachments.length === 0}
|
||||
disabled={!canSend || (!draft.trim() && attach.attachments.length === 0)}
|
||||
onClick={submit}
|
||||
>
|
||||
<Send size={18} />
|
||||
|
||||
@@ -42,6 +42,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
import { KpiStrip, PageContainer } from "@/components/page";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import {
|
||||
autoFillPlacements,
|
||||
mergePlacementsWithSaved,
|
||||
@@ -102,6 +104,7 @@ const parseError = (error: unknown, fallback: string) => {
|
||||
};
|
||||
|
||||
export default function TrainScheduleV2DetailPage() {
|
||||
const { user: authUser } = useAuth();
|
||||
const { scheduleId } = useParams<{ scheduleId: string }>();
|
||||
const { toast } = useToast();
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
@@ -394,7 +397,9 @@ export default function TrainScheduleV2DetailPage() {
|
||||
: [];
|
||||
|
||||
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
|
||||
// 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 { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { canCreateSchedule, canUpdateSchedule } from "@/lib/permissions";
|
||||
import { FREIGHT_PERMS, canCreateSchedule, hasPermission } from "@/lib/permissions";
|
||||
import type {
|
||||
CreateScheduleWindowRulePayload,
|
||||
FreightType,
|
||||
@@ -111,7 +111,7 @@ export default function TrainScheduleV2ListPage() {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const canCreate = canCreateSchedule(user);
|
||||
const canUpdate = canUpdateSchedule(user);
|
||||
const canDispatch = hasPermission(user, FREIGHT_PERMS.trainScheduling.dispatch);
|
||||
const { viewMode, setViewMode } = useFleetViewMode("train-scheduling-v2");
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -491,7 +491,7 @@ export default function TrainScheduleV2ListPage() {
|
||||
{/* Start the run. Same transition as the detail page's
|
||||
Dispatch button — that page also shows unassigned-wagon
|
||||
and not-loaded warnings, so it stays the fuller surface. */}
|
||||
{canUpdate && schedule.status === "SCHEDULED" ? (
|
||||
{canDispatch && schedule.status === "SCHEDULED" ? (
|
||||
<Menu.Item
|
||||
leftSection={<Play size={15} />}
|
||||
onClick={() => setDispatchTarget(schedule)}
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
import { openPdfBlob } from '@/components/warehouses/pdf';
|
||||
import { buildWarehouseExitPaperPdf } from '@/components/warehouses/warehousePdf';
|
||||
import { extractErrorMessage } from '@/components/warehouses/options';
|
||||
import { useAuth } from '@/auth/useAuth';
|
||||
import { FREIGHT_PERMS, hasPermission } from '@/lib/permissions';
|
||||
|
||||
const STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
||||
DRAFT: 'gray',
|
||||
@@ -171,6 +173,9 @@ export default function WarehouseInvoicesPage() {
|
||||
}
|
||||
|
||||
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 navigate = useNavigate();
|
||||
const { data: inv, isLoading } = useQuery(
|
||||
@@ -430,9 +435,11 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
||||
placeholder="Optional"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button leftSection={<CreditCard size={16} />} loading={payOnline.isPending} onClick={handleOnlinePay}>
|
||||
Pay with {gatewayMethod === 'WAAFI' ? 'Waafi' : 'Telebirr'}
|
||||
</Button>
|
||||
{mayRecordPayment && (
|
||||
<Button leftSection={<CreditCard size={16} />} loading={payOnline.isPending} onClick={handleOnlinePay}>
|
||||
Pay with {gatewayMethod === 'WAAFI' ? 'Waafi' : 'Telebirr'}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<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)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
|
||||
Pay
|
||||
</Button>
|
||||
{mayRecordPayment && (
|
||||
<Button leftSection={<CreditCard size={16} />} loading={pay.isPending} onClick={handlePay}>
|
||||
Pay
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
@@ -506,7 +515,7 @@ function InvoiceDetailModal({ id, onClose }: { id: string | null; onClose: () =>
|
||||
Gate clearance & exit paper
|
||||
</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}>
|
||||
Cancel invoice
|
||||
</Button>
|
||||
|
||||
BIN
apps/edr-freight-web/portal/public/assets/edr-portal-guide.webm
Normal file
BIN
apps/edr-freight-web/portal/public/assets/edr-portal-guide.webm
Normal file
Binary file not shown.
@@ -72,8 +72,33 @@ export default function HelpPage() {
|
||||
<DocShell
|
||||
current="/help"
|
||||
title="Help & Support"
|
||||
subtitle="Get answers fast — browse the common topics, check the FAQ, or reach our team directly."
|
||||
subtitle="Get answers fast — watch the walkthrough, browse the common topics, check the FAQ, or reach our team directly."
|
||||
>
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xl font-bold tracking-tight">
|
||||
Portal walkthrough
|
||||
</h2>
|
||||
<p className="mt-2 leading-7 text-muted-foreground">
|
||||
A guided tour of the portal — registering your company, raising a
|
||||
booking against a contract, and settling an invoice.
|
||||
</p>
|
||||
|
||||
{/* preload="metadata" so the 28 MB file is not pulled on every visit;
|
||||
the browser fetches it only once playback starts. */}
|
||||
<video
|
||||
controls
|
||||
preload="metadata"
|
||||
className="mt-6 w-full rounded-[32px] border border-border bg-black"
|
||||
>
|
||||
<source src="/assets/edr-portal-guide.webm" type="video/webm" />
|
||||
Your browser cannot play this video. Download it at{" "}
|
||||
<a href="/assets/edr-portal-guide.webm">
|
||||
/assets/edr-portal-guide.webm
|
||||
</a>
|
||||
.
|
||||
</video>
|
||||
</section>
|
||||
|
||||
{/* Live chat is the fastest route, so lead with it. */}
|
||||
<div className="rounded-[32px] border border-border bg-card p-8">
|
||||
<div className="flex items-start gap-4">
|
||||
|
||||
Reference in New Issue
Block a user