diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 567ecf074..e0c47414d 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -12,6 +12,7 @@ import { PackageCheck, PackageOpen, Paperclip, + Receipt, Send, Settings, ShieldCheck, @@ -32,7 +33,11 @@ import { useParams, } from "react-router-dom"; -import { FreightDashboardLayout, type SidebarItem, type SidebarSection } from "@/components/layout"; +import { + FreightDashboardLayout, + type SidebarItem, + type SidebarSection, +} from "@/components/layout"; import { useAuth } from "./auth/useAuth"; import LoadingScreen from "./components/LoadingScreen"; import LoginPage from "./pages/auth/LoginPage"; @@ -53,6 +58,8 @@ import GlCreateBookingForm from "./components/contracts/GlCreateBookingForm"; import DocumentClearanceDetailPage from "./pages/bookings/DocumentClearanceDetailPage"; import CustomerDetailPage from "./pages/customers/CustomerDetailPage"; import CustomersPage from "./pages/customers/CustomersPage"; +import InvoiceDetailPage from "./pages/invoices/InvoiceDetailPage"; +import InvoicesPage from "./pages/invoices/InvoicesPage"; import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; import MyProfilePage from "./pages/dashboard/MyProfilePage"; @@ -61,7 +68,10 @@ import UserManagementHostPage from "./pages/dashboard/user-management/UserManage import PaymentsPage from "./pages/payments/PaymentsPage"; //import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage"; import { RequirePermission } from "./components/auth/RequirePermission"; -import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "./lib/permissions"; +import { + FREIGHT_PERMS, + hasPermission as hasFreightPermission, +} from "./lib/permissions"; import PermissionsPage from "./pages/dashboard/user-management/PermissionsPage"; import PositionTypesPage from "./pages/dashboard/user-management/PositionTypesPage"; import RolesPage from "./pages/dashboard/user-management/RolesPage"; @@ -144,6 +154,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ icon: , permission: FREIGHT_PERMS.bookings.view, }, + { + label: "Invoices", + href: "/dashboard/invoices", + icon: , + permission: FREIGHT_PERMS.bookings.view, + }, ...demoItems, ], }, @@ -506,7 +522,10 @@ const App = () => { } /> } /> } /> - } /> + } + /> }> } /> } /> @@ -522,8 +541,27 @@ const App = () => { /> } /> } /> + + + + } + /> + + + + } + /> } /> - } /> + } + /> } @@ -536,7 +574,9 @@ const App = () => { + } @@ -570,7 +610,9 @@ const App = () => { + } @@ -578,7 +620,9 @@ const App = () => { + } @@ -586,7 +630,9 @@ const App = () => { + } @@ -618,12 +664,20 @@ const App = () => { } /> - } /> - } /> + } + /> + } + /> + } @@ -631,7 +685,9 @@ const App = () => { + } @@ -644,7 +700,9 @@ const App = () => { + } @@ -655,155 +713,174 @@ const App = () => { /> } /> } /> - } /> + } + /> } /> } /> } /> } /> } /> } /> - } /> - } /> + } + /> + } + /> } /> } /> - } /> - } /> + } + /> + } + /> - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> } + element={ + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + } /> { {/* Legacy embedded user management routes */} } /> } /> - } /> + } + /> {/* } /> */} - } /> + } + /> } /> { } + element={ + + } /> { } /> } /> - } /> - } /> + } + /> + } + /> { } + element={ + + } + /> + } /> - } /> } /> } /> @@ -1026,9 +1122,7 @@ const App = () => { /** Redirect removed milestones page to document clearance. */ function BookingMilestonesRedirect() { const { id } = useParams(); - return ( - - ); + return ; } /** Redirect legacy GL Ethiopia clearance URLs to the unified document clearance hub. */ diff --git a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts index 2221fe898..cb30ad30b 100644 --- a/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/QUERY_KEYS.ts @@ -3,6 +3,7 @@ import type { BookingListFilter } from "@/services/bookings.service"; import type { ContractListFilter } from "@/services/contracts.service"; import type { RuleEngineListParams } from "@/services/ruleEngine/ruleEngine.service"; import type { CompanyListFilter } from "@/types/customer"; +import type { InvoiceListFilter } from "@/types/invoice"; import type { RuleEngineResourceSlug } from "@/types/rule-engine"; import type { TrainScheduleFilters } from "@/types/trainScheduling"; @@ -16,7 +17,8 @@ export const QUERY_KEYS = { ROOT: ["file-upload-settings"] as const, list: () => ["file-upload-settings", "list"] as const, byId: (id: string) => ["file-upload-settings", "detail", id] as const, - byCode: (code: string) => ["file-upload-settings", "by-code", code] as const, + byCode: (code: string) => + ["file-upload-settings", "by-code", code] as const, }, DROPDOWN_SETTINGS: { @@ -33,10 +35,18 @@ export const QUERY_KEYS = { ["customers", "list", filter ?? {}] as const, byId: (id: string) => ["customers", "detail", id] as const, bookings: (id: string) => ["customers", "detail", id, "bookings"] as const, - documents: (id: string) => ["customers", "detail", id, "documents"] as const, + documents: (id: string) => + ["customers", "detail", id, "documents"] as const, payments: (id: string) => ["customers", "detail", id, "payments"] as const, }, + INVOICES: { + ROOT: ["invoices"] as const, + list: (filter?: InvoiceListFilter) => + ["invoices", "list", filter ?? {}] as const, + byId: (id: string) => ["invoices", "detail", id] as const, + }, + BOOKINGS: { ROOT: ["bookings"] as const, list: (filter?: BookingListFilter) => @@ -79,7 +89,12 @@ export const QUERY_KEYS = { TRAIN_SCHEDULING: { ROOT: ["train-scheduling"] as const, eligible: (freightType?: string, filters?: TrainScheduleFilters) => - ["train-scheduling", "eligible-bookings", freightType ?? "CONTAINER", filters ?? {}] as const, + [ + "train-scheduling", + "eligible-bookings", + freightType ?? "CONTAINER", + filters ?? {}, + ] as const, locomotives: (routeId?: string) => ["train-scheduling", "locomotives", routeId ?? "all"] as const, stations: () => ["train-scheduling", "stations"] as const, @@ -97,31 +112,37 @@ export const QUERY_KEYS = { FLEET: { ROOT: ["fleet"] as const, - list: (resource: FleetResourceSlug | string) => ["fleet", "list", resource] as const, + list: (resource: FleetResourceSlug | string) => + ["fleet", "list", resource] as const, }, VEHICLES: { ROOT: ["vehicles"] as const, - list: (filter?: Record) => ["vehicles", "list", filter ?? {}] as const, + list: (filter?: Record) => + ["vehicles", "list", filter ?? {}] as const, byId: (id: string) => ["vehicles", "detail", id] as const, }, FIRST_MILE: { ROOT: ["first-mile"] as const, - list: (filter?: Record) => ["first-mile", "list", filter ?? {}] as const, + list: (filter?: Record) => + ["first-mile", "list", filter ?? {}] as const, byId: (id: string) => ["first-mile", "detail", id] as const, }, LAST_MILE: { ROOT: ["last-mile"] as const, - list: (filter?: Record) => ["last-mile", "list", filter ?? {}] as const, + list: (filter?: Record) => + ["last-mile", "list", filter ?? {}] as const, byId: (id: string) => ["last-mile", "detail", id] as const, }, RULE_ENGINE: { ROOT: ["rule-engine"] as const, - list: (resource: RuleEngineResourceSlug | string, params?: RuleEngineListParams) => - ["rule-engine", "list", resource, params ?? {}] as const, + list: ( + resource: RuleEngineResourceSlug | string, + params?: RuleEngineListParams, + ) => ["rule-engine", "list", resource, params ?? {}] as const, detail: (resource: RuleEngineResourceSlug | string, id: string) => ["rule-engine", "detail", resource, id] as const, chain: ["rule-engine", "approval-rules", "chain"] as const, @@ -135,27 +156,39 @@ export const QUERY_KEYS = { OVERVIEW: { ROOT: ["overview"] as const, - dashboard: (range?: string) => ["overview", "dashboard", range ?? "30d"] as const, - bookingsTab: (range?: string) => ["overview", "bookings", range ?? "30d"] as const, - contractsTab: (range?: string) => ["overview", "contracts", range ?? "30d"] as const, - billingTab: (range?: string) => ["overview", "billing", range ?? "30d"] as const, + dashboard: (range?: string) => + ["overview", "dashboard", range ?? "30d"] as const, + bookingsTab: (range?: string) => + ["overview", "bookings", range ?? "30d"] as const, + contractsTab: (range?: string) => + ["overview", "contracts", range ?? "30d"] as const, + billingTab: (range?: string) => + ["overview", "billing", range ?? "30d"] as const, operationsTab: () => ["overview", "operations"] as const, - customersTab: (range?: string) => ["overview", "customers", range ?? "30d"] as const, - staffTab: (range?: string) => ["overview", "staff", range ?? "30d"] as const, + customersTab: (range?: string) => + ["overview", "customers", range ?? "30d"] as const, + staffTab: (range?: string) => + ["overview", "staff", range ?? "30d"] as const, }, FUEL: { ROOT: ["fuel"] as const, - purchases: (vehicleId?: string) => ["fuel", "purchases", vehicleId ?? "all"] as const, - stats: (vehicleId?: string) => ["fuel", "stats", vehicleId ?? "all"] as const, + purchases: (vehicleId?: string) => + ["fuel", "purchases", vehicleId ?? "all"] as const, + stats: (vehicleId?: string) => + ["fuel", "stats", vehicleId ?? "all"] as const, }, MAINTENANCE: { ROOT: ["maintenance"] as const, - schedules: (vehicleId?: string) => ["maintenance", "schedules", vehicleId ?? "all"] as const, - upcoming: (vehicleId?: string) => ["maintenance", "upcoming", vehicleId ?? "all"] as const, - history: (vehicleId?: string) => ["maintenance", "history", vehicleId ?? "all"] as const, - stats: (vehicleId?: string) => ["maintenance", "stats", vehicleId ?? "all"] as const, + schedules: (vehicleId?: string) => + ["maintenance", "schedules", vehicleId ?? "all"] as const, + upcoming: (vehicleId?: string) => + ["maintenance", "upcoming", vehicleId ?? "all"] as const, + history: (vehicleId?: string) => + ["maintenance", "history", vehicleId ?? "all"] as const, + stats: (vehicleId?: string) => + ["maintenance", "stats", vehicleId ?? "all"] as const, }, FINANCIAL_REPORTS: { diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index dff9167f5..abac14b04 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -13,7 +13,7 @@ export const URL_CONSTANTS = { BASE: "/users", BY_ID: (id: string | number) => `/users/${id}`, SET_PASSWORD: "/api/auth/set-password", - ME: "/api/auth/me" + ME: "/api/auth/me", }, ROLES: { @@ -74,9 +74,18 @@ export const URL_CONSTANTS = { STATS: "/companies/stats", BY_ID: (id: string | number) => `/companies/${id}`, DOCUMENTS: (id: string) => `/companies/${id}/documents`, - PROFILE_STATUS: (profileId: string) => `/companies/company-profiles/${profileId}/status`, - BOOKINGS_CUSTOMER_VIEW: (id: string) => `/bookings/by-company/${id}/customer-view`, - PAYMENTS_CUSTOMER_VIEW: (id: string) => `/payments/by-company/${id}/customer-view`, + PROFILE_STATUS: (profileId: string) => + `/companies/company-profiles/${profileId}/status`, + BOOKINGS_CUSTOMER_VIEW: (id: string) => + `/bookings/by-company/${id}/customer-view`, + PAYMENTS_CUSTOMER_VIEW: (id: string) => + `/payments/by-company/${id}/customer-view`, + }, + + BILLING: { + INVOICES: "/billing/invoices", + INVOICE_BY_ID: (id: string) => `/billing/invoices/${id}`, + INVOICE_DOCUMENT: (id: string) => `/billing/invoices/${id}/document`, }, CUSTOMERS_API: { @@ -124,15 +133,21 @@ export const URL_CONSTANTS = { CANCEL: (id: string) => `/bookings/${id}/cancel`, CONSOLIDATION: (id: string) => `/bookings/${id}/consolidation`, CLEARANCE: (id: string) => `/bookings/${id}/clearance`, - CLEARANCE_DECLARATION: (id: string) => `/bookings/${id}/clearance/declaration`, + CLEARANCE_DECLARATION: (id: string) => + `/bookings/${id}/clearance/declaration`, CLEARANCE_DUTY: (id: string) => `/bookings/${id}/clearance/duty`, CLEARANCE_FINALIZE_PRE: (id: string) => `/bookings/${id}/clearance/finalize-pre-clearance`, - CLEARANCE_TRANSIT_PERMIT: (id: string) => `/bookings/${id}/clearance/transit-permit`, - CLEARANCE_DELIVERY_ORDER: (id: string) => `/bookings/${id}/clearance/delivery-order`, - CLEARANCE_RELEASE_ORDER: (id: string) => `/bookings/${id}/clearance/release-order`, - CLEARANCE_RO_AMENDMENT: (id: string) => `/bookings/${id}/clearance/ro-amendment`, - CLEARANCE_EXPORT_RELEASE: (id: string) => `/bookings/${id}/clearance/export-release`, + CLEARANCE_TRANSIT_PERMIT: (id: string) => + `/bookings/${id}/clearance/transit-permit`, + CLEARANCE_DELIVERY_ORDER: (id: string) => + `/bookings/${id}/clearance/delivery-order`, + CLEARANCE_RELEASE_ORDER: (id: string) => + `/bookings/${id}/clearance/release-order`, + CLEARANCE_RO_AMENDMENT: (id: string) => + `/bookings/${id}/clearance/ro-amendment`, + CLEARANCE_EXPORT_RELEASE: (id: string) => + `/bookings/${id}/clearance/export-release`, CLEARANCE_ET_QUEUE: "/bookings/clearance/et-queue", CLEARANCE_DJ_QUEUE: "/bookings/clearance/dj-queue", }, @@ -157,7 +172,8 @@ export const URL_CONSTANTS = { CLEARANCE_OUTPUT_DOCUMENTS: (id: string) => `/contracts/${id}/clearance/output-documents`, CLEARANCE_FINALIZE: (id: string) => `/contracts/${id}/clearance/finalize`, - CLEARANCE_DECLARATION: (id: string) => `/contracts/${id}/clearance/declaration`, + CLEARANCE_DECLARATION: (id: string) => + `/contracts/${id}/clearance/declaration`, CLEARANCE_DUTY: (id: string) => `/contracts/${id}/clearance/duty`, CLEARANCE_DUTY_SLIP: (id: string) => `/contracts/${id}/clearance/duty-slip`, CLEARANCE_FINALIZE_PRE: (id: string) => @@ -232,7 +248,7 @@ export const URL_CONSTANTS = { }, ROUTES: { - BASE: '/routes', + BASE: "/routes", BY_ID: (id: string) => `/routes/${id}`, }, @@ -246,10 +262,12 @@ export const URL_CONSTANTS = { BATCH_BOARD_DETAIL: (scheduleId: string) => `/train-scheduling/batch-board/${scheduleId}`, RUN_BATCH: (id: string) => `/train-scheduling/schedules/${id}/run-batch`, - RUN_ALLOCATION: (id: string) => `/train-scheduling/schedules/${id}/run-allocation`, + RUN_ALLOCATION: (id: string) => + `/train-scheduling/schedules/${id}/run-allocation`, ASSIGN_UNASSIGNED_BOOKING: (id: string) => `/train-scheduling/schedules/${id}/assign-unassigned-booking`, - BOOKING_WINDOW: (id: string) => `/train-scheduling/schedules/${id}/booking-window`, + BOOKING_WINDOW: (id: string) => + `/train-scheduling/schedules/${id}/booking-window`, MARK_BOOKING_PAID: (bookingId: string) => `/train-scheduling/bookings/${bookingId}/mark-paid`, EXPIRE_BOOKING: (bookingId: string) => @@ -258,12 +276,14 @@ export const URL_CONSTANTS = { `/train-scheduling/bookings/${bookingId}/move-schedule`, GLOBAL_RULES: "/train-scheduling/global-rules", PREVIEW: "/train-scheduling/preview", - ASSIGN_BOOKINGS: (id: string) => `/train-scheduling/schedules/${id}/assign-bookings`, + ASSIGN_BOOKINGS: (id: string) => + `/train-scheduling/schedules/${id}/assign-bookings`, CONTAINER: { ELIGIBLE_BOOKINGS: "/train-scheduling/container/eligible-bookings", PREVIEW: "/train-scheduling/container/preview", SCHEDULES: "/train-scheduling/container/schedules", - SCHEDULE_BY_ID: (id: string) => `/train-scheduling/container/schedules/${id}`, + SCHEDULE_BY_ID: (id: string) => + `/train-scheduling/container/schedules/${id}`, ASSIGN_BOOKINGS: (id: string) => `/train-scheduling/container/schedules/${id}/assign-bookings`, CANCEL_SCHEDULE: (id: string) => @@ -302,15 +322,18 @@ export const URL_CONSTANTS = { `/train-scheduling/schedules/${id}/import-djibouti/load-list/document`, EXPORT_LOAD_LIST_DOCUMENT: (id: string) => `/train-scheduling/schedules/${id}/export/load-list/document`, - CHECKPOINTS: (id: string) => `/train-scheduling/schedules/${id}/checkpoints`, + CHECKPOINTS: (id: string) => + `/train-scheduling/schedules/${id}/checkpoints`, ARRIVE: (id: string) => `/train-scheduling/schedules/${id}/arrive`, RESCHEDULE_PREVIEW: (id: string) => `/train-scheduling/schedules/${id}/reschedule/preview`, RESCHEDULE_EXECUTE: (id: string) => `/train-scheduling/schedules/${id}/reschedule/execute`, - MAINTENANCE: (id: string) => `/train-scheduling/schedules/${id}/maintenance`, + MAINTENANCE: (id: string) => + `/train-scheduling/schedules/${id}/maintenance`, SCHEDULES: "/train-scheduling/container/schedules", - SCHEDULE_BY_ID: (id: string) => `/train-scheduling/container/schedules/${id}`, + SCHEDULE_BY_ID: (id: string) => + `/train-scheduling/container/schedules/${id}`, CANCEL_SCHEDULE: (id: string) => `/train-scheduling/container/schedules/${id}/cancel`, REMOVE_WAGON_SLOT: (scheduleId: string, wagonId: string) => @@ -358,175 +381,193 @@ export const URL_CONSTANTS = { APPROVAL_RULE_BY_ID: (id: string) => `/approval-rules/${id}`, APPROVAL_RULES_CHAIN: "/approval-rules/chain", }, - RATE_MATRIX: { - BASE: '/api/rate-matrices', - DRAFT: '/api/rate-matrices/draft', + RATE_MATRIX: { + BASE: "/api/rate-matrices", + DRAFT: "/api/rate-matrices/draft", SUBMIT: (id: string) => `/api/rate-matrices/${id}/submit`, AUTHORIZE: (id: string) => `/api/rate-matrices/${id}/authorize`, - LIST: '/api/rate-matrices', + LIST: "/api/rate-matrices", DETAIL: (id: string) => `/api/rate-matrices/${id}`, }, REFERENCE: { - PORTS: '/api/reference/ports', - CITIES: '/api/reference/cities', - CONTAINER_TYPES: '/api/reference/container-types', - CURRENCIES: '/api/reference/currencies', + PORTS: "/api/reference/ports", + CITIES: "/api/reference/cities", + CONTAINER_TYPES: "/api/reference/container-types", + CURRENCIES: "/api/reference/currencies", }, FACILITIES: { - BASE: '/facilities', + BASE: "/facilities", BY_ID: (id: string) => `/facilities/${id}`, }, WAREHOUSES: { - BASE: '/warehouses', - DASHBOARD: '/warehouses/dashboard', + BASE: "/warehouses", + DASHBOARD: "/warehouses/dashboard", BY_ID: (id: string) => `/warehouses/${id}`, YARDS: (warehouseId: string) => `/warehouses/${warehouseId}/yards`, }, WAREHOUSE_YARDS: { - BASE: '/warehouse-yards', + BASE: "/warehouse-yards", BY_ID: (id: string) => `/warehouse-yards/${id}`, ZONES: (yardId: string) => `/warehouse-yards/${yardId}/zones`, }, WAREHOUSE_ZONES: { - BASE: '/warehouse-zones', + BASE: "/warehouse-zones", BY_ID: (id: string) => `/warehouse-zones/${id}`, }, WAREHOUSE_INVENTORY: { - BASE: '/warehouse-inventory', - RECEIVE: '/warehouse-inventory/receive', - DASHBOARD_SUMMARY: '/warehouse-inventory/dashboard/summary', - READY_FOR_LOADING: '/warehouse-inventory/ready-for-loading', - INQUIRY: '/warehouse-inventory/inquiry', + BASE: "/warehouse-inventory", + RECEIVE: "/warehouse-inventory/receive", + DASHBOARD_SUMMARY: "/warehouse-inventory/dashboard/summary", + READY_FOR_LOADING: "/warehouse-inventory/ready-for-loading", + INQUIRY: "/warehouse-inventory/inquiry", STORE: (id: string) => `/warehouse-inventory/${id}/store`, INSPECT: (id: string) => `/warehouse-inventory/${id}/inspect`, MARK_READY: (id: string) => `/warehouse-inventory/${id}/ready-for-loading`, LOAD: (id: string) => `/warehouse-inventory/${id}/load`, DISPATCH: (id: string) => `/warehouse-inventory/${id}/dispatch`, MOVE: (id: string) => `/warehouse-inventory/${id}/move`, - RESERVE: '/warehouse-inventory/reserve', - ARRIVAL_QUEUE: '/warehouse-inventory/arrival-queue', - AUTO_UNLOAD_ARRIVED: '/warehouse-inventory/auto-unload-arrived', - AUTO_LOAD_READY: '/warehouse-inventory/auto-load-ready', - UNLOAD_BOOKING: (bookingId: string) => `/warehouse-inventory/bookings/${bookingId}/unload`, - INSPECTION_REPORTS: (inventoryId: string) => `/warehouse-inventory/${inventoryId}/inspection-reports`, - LOADABLE_WAGONS: '/warehouse-inventory/loadable-wagons', - BOOKING_SCHEDULE: (bookingId: string) => `/warehouse-inventory/booking/${bookingId}/schedule`, + RESERVE: "/warehouse-inventory/reserve", + ARRIVAL_QUEUE: "/warehouse-inventory/arrival-queue", + AUTO_UNLOAD_ARRIVED: "/warehouse-inventory/auto-unload-arrived", + AUTO_LOAD_READY: "/warehouse-inventory/auto-load-ready", + UNLOAD_BOOKING: (bookingId: string) => + `/warehouse-inventory/bookings/${bookingId}/unload`, + INSPECTION_REPORTS: (inventoryId: string) => + `/warehouse-inventory/${inventoryId}/inspection-reports`, + LOADABLE_WAGONS: "/warehouse-inventory/loadable-wagons", + BOOKING_SCHEDULE: (bookingId: string) => + `/warehouse-inventory/booking/${bookingId}/schedule`, MOVEMENTS: (id: string) => `/warehouse-inventory/${id}/movements`, ACTIVITY: (id: string) => `/warehouse-inventory/${id}/activity`, LOADINGS: (id: string) => `/warehouse-inventory/${id}/loadings`, // Import branch - MARK_READY_PICKUP: (id: string) => `/warehouse-inventory/${id}/ready-for-pickup`, + MARK_READY_PICKUP: (id: string) => + `/warehouse-inventory/${id}/ready-for-pickup`, RELEASE: (id: string) => `/warehouse-inventory/${id}/release`, - RELEASE_DOCUMENT: (id: string) => `/warehouse-inventory/${id}/release-document`, + RELEASE_DOCUMENT: (id: string) => + `/warehouse-inventory/${id}/release-document`, GRN_DOCUMENT: (id: string) => `/warehouse-inventory/${id}/grn-document`, - HANDOVER_DOCUMENT: (id: string) => `/warehouse-inventory/${id}/handover-document`, + HANDOVER_DOCUMENT: (id: string) => + `/warehouse-inventory/${id}/handover-document`, DELIVER: (id: string) => `/warehouse-inventory/${id}/deliver`, // Receive (Import/Export bulk) ELIGIBLE_BOOKINGS: (direction?: string) => direction ? `/warehouse-inventory/eligible-bookings?direction=${direction}` : `/warehouse-inventory/eligible-bookings`, - RECEIVE_BULK: '/warehouse-inventory/receive-bulk', - LOAD_PASSED_EXPORT: '/warehouse-inventory/load-passed-export', - BULK_MARK_INSPECTED: '/warehouse-inventory/bulk-mark-inspected', - RECEIVED_EXPORT: '/warehouse-inventory/received-export', - READY_TO_LOAD_EXPORT: '/warehouse-inventory/ready-to-load-export', - LOADED_EXPORT: '/warehouse-inventory/loaded-export', - BULK_DISPATCH_EXPORT: '/warehouse-inventory/bulk-dispatch-export', - IMPORT_ARRIVE_QUEUE: '/warehouse-inventory/import/arrive-queue', + RECEIVE_BULK: "/warehouse-inventory/receive-bulk", + LOAD_PASSED_EXPORT: "/warehouse-inventory/load-passed-export", + BULK_MARK_INSPECTED: "/warehouse-inventory/bulk-mark-inspected", + RECEIVED_EXPORT: "/warehouse-inventory/received-export", + READY_TO_LOAD_EXPORT: "/warehouse-inventory/ready-to-load-export", + LOADED_EXPORT: "/warehouse-inventory/loaded-export", + BULK_DISPATCH_EXPORT: "/warehouse-inventory/bulk-dispatch-export", + IMPORT_ARRIVE_QUEUE: "/warehouse-inventory/import/arrive-queue", IMPORT_TRAIN_ITEMS: (scheduleId: string) => `/warehouse-inventory/import/trains/${scheduleId}/items`, - IMPORT_AUTO_UNLOAD_ARRIVED: '/warehouse-inventory/import/auto-unload-arrived-bookings', - IMPORT_UNLOADED_QUEUE: '/warehouse-inventory/import/unloaded-queue', - IMPORT_PICKUP_READY_QUEUE: '/warehouse-inventory/import/pickup-ready-queue', - EXPORT_DJIBOUTI_ARRIVAL_QUEUE: '/warehouse-inventory/export/djibouti-arrival-queue', + IMPORT_AUTO_UNLOAD_ARRIVED: + "/warehouse-inventory/import/auto-unload-arrived-bookings", + IMPORT_UNLOADED_QUEUE: "/warehouse-inventory/import/unloaded-queue", + IMPORT_PICKUP_READY_QUEUE: "/warehouse-inventory/import/pickup-ready-queue", + EXPORT_DJIBOUTI_ARRIVAL_QUEUE: + "/warehouse-inventory/export/djibouti-arrival-queue", EXPORT_DJIBOUTI_TRAIN_ITEMS: (scheduleId: string) => `/warehouse-inventory/export/djibouti-trains/${scheduleId}/items`, - EXPORT_AUTO_UNLOAD_AT_DJIBOUTI: '/warehouse-inventory/export/auto-unload-at-djibouti', + EXPORT_AUTO_UNLOAD_AT_DJIBOUTI: + "/warehouse-inventory/export/auto-unload-at-djibouti", }, WAREHOUSE_LOADINGS: { - BASE: '/warehouse-loadings', + BASE: "/warehouse-loadings", }, WAREHOUSE_INSPECTION: { BY_ID: (id: string) => `/warehouse-inspection-reports/${id}`, - ATTACHMENTS: (id: string) => `/warehouse-inspection-reports/${id}/attachments`, + ATTACHMENTS: (id: string) => + `/warehouse-inspection-reports/${id}/attachments`, }, WAREHOUSE_RULES: { - ALLOCATION: '/warehouse-allocation-rules', + ALLOCATION: "/warehouse-allocation-rules", ALLOCATION_BY_ID: (id: string) => `/warehouse-allocation-rules/${id}`, - ALLOCATION_PREVIEW: '/warehouse-allocation/preview', - FEES: '/warehouse-fee-rules', + ALLOCATION_PREVIEW: "/warehouse-allocation/preview", + FEES: "/warehouse-fee-rules", FEES_BY_ID: (id: string) => `/warehouse-fee-rules/${id}`, - FEE_PREVIEW: (inventoryId: string) => `/warehouse-inventory/${inventoryId}/fee-preview`, + FEE_PREVIEW: (inventoryId: string) => + `/warehouse-inventory/${inventoryId}/fee-preview`, }, WAREHOUSE_INVOICES: { - BASE: '/warehouse-fee-invoices', + BASE: "/warehouse-fee-invoices", BY_ID: (id: string) => `/warehouse-fee-invoices/${id}`, DOCUMENT: (id: string) => `/warehouse-fee-invoices/${id}/document`, RECEIPT: (id: string) => `/warehouse-fee-invoices/${id}/receipt`, CANCEL: (id: string) => `/warehouse-fee-invoices/${id}/cancel`, PAY: (id: string) => `/warehouse-fee-invoices/${id}/pay`, PAY_ONLINE: (id: string) => `/warehouse-fee-invoices/${id}/pay-online`, - GENERATE: (inventoryId: string) => `/warehouse-inventory/${inventoryId}/generate-fee-invoice`, - FOR_INVENTORY: (inventoryId: string) => `/warehouse-inventory/${inventoryId}/fee-invoices`, - FOR_BOOKING: (bookingId: string) => `/bookings/${bookingId}/warehouse-fee-invoices`, - GATE_CLEARANCE: (inventoryId: string) => `/warehouse-inventory/${inventoryId}/gate-clearance`, + GENERATE: (inventoryId: string) => + `/warehouse-inventory/${inventoryId}/generate-fee-invoice`, + FOR_INVENTORY: (inventoryId: string) => + `/warehouse-inventory/${inventoryId}/fee-invoices`, + FOR_BOOKING: (bookingId: string) => + `/bookings/${bookingId}/warehouse-fee-invoices`, + GATE_CLEARANCE: (inventoryId: string) => + `/warehouse-inventory/${inventoryId}/gate-clearance`, }, INTERCHANGE_DOCUMENTS: { - BASE: '/interchange-documents', + BASE: "/interchange-documents", BY_ID: (id: string) => `/interchange-documents/${id}`, - GENERATE_FROM_SCHEDULE: '/interchange-documents/generate-from-schedule', + GENERATE_FROM_SCHEDULE: "/interchange-documents/generate-from-schedule", ACKNOWLEDGE: (id: string) => `/interchange-documents/${id}/acknowledge`, DISPUTE: (id: string) => `/interchange-documents/${id}/dispute`, CANCEL: (id: string) => `/interchange-documents/${id}/cancel`, }, IMPORT_OPERATIONS: { - DJIBOUTI_INCIDENTS: '/import-operations/djibouti-incidents', + DJIBOUTI_INCIDENTS: "/import-operations/djibouti-incidents", CUSTOMS: (bookingId: string) => `/import-operations/customs/${bookingId}`, - CUSTOMS_DOCUMENTS: (bookingId: string) => `/import-operations/customs/${bookingId}/documents`, - CUSTOMS_DECLARATION: (bookingId: string) => `/import-operations/customs/${bookingId}/declaration`, + CUSTOMS_DOCUMENTS: (bookingId: string) => + `/import-operations/customs/${bookingId}/documents`, + CUSTOMS_DECLARATION: (bookingId: string) => + `/import-operations/customs/${bookingId}/declaration`, CUSTOMS_NOTIFY_DUTIES_TAXES: (bookingId: string) => `/import-operations/customs/${bookingId}/notify-duties-taxes`, CUSTOMS_DUTIES_TAXES_PAID: (bookingId: string) => `/import-operations/customs/${bookingId}/duties-taxes-paid`, - CUSTOMS_RISK: (bookingId: string) => `/import-operations/customs/${bookingId}/risk`, + CUSTOMS_RISK: (bookingId: string) => + `/import-operations/customs/${bookingId}/risk`, CUSTOMS_RELEASE_PERMITTED: (bookingId: string) => `/import-operations/customs/${bookingId}/release-permitted`, - EMPTY_CONTAINER_RETURNS: '/import-operations/empty-container-returns', + EMPTY_CONTAINER_RETURNS: "/import-operations/empty-container-returns", EMPTY_CONTAINER_RETURN_STATUS: (id: string) => `/import-operations/empty-container-returns/${id}/status`, }, VEHICLES: { - BASE: '/vehicles', + BASE: "/vehicles", BY_ID: (id: string) => `/vehicles/${id}`, }, FIRST_MILE: { - BASE: '/first-mile', + BASE: "/first-mile", BY_ID: (id: string) => `/first-mile/${id}`, ACCEPT: (reference: string) => `/first-mile/accept/${reference}`, }, LAST_MILE: { - BASE: '/last-mile', + BASE: "/last-mile", BY_ID: (id: string) => `/last-mile/${id}`, ACCEPT: (reference: string) => `/last-mile/accept/${reference}`, }, DRIVERS: { - BASE: '/drivers', + BASE: "/drivers", BY_ID: (id: string) => `/drivers/${id}`, }, }; diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoiceDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoiceDetailPage.tsx new file mode 100644 index 000000000..7bafc093e --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoiceDetailPage.tsx @@ -0,0 +1,254 @@ +import { + ActionIcon, + Button, + Card, + Center, + Container, + Group, + Loader, + SimpleGrid, + Stack, + Table, + Text, +} from "@mantine/core"; +import { useQuery } from "@tanstack/react-query"; +import { ArrowLeft, Download } from "lucide-react"; +import { useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; + +import { + InvoiceStatusBadge, + formatDate, + formatMoney, + humanize, +} from "@/components/customers"; +import { PageContainer, PageHeader } from "@/components/page"; +import { api } from "@/services/api"; +import { invoicesService } from "@/services/invoices.service"; + +function openPdfBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const opened = window.open(url, "_blank"); + if (!opened) { + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + } + setTimeout(() => URL.revokeObjectURL(url), 60_000); +} + +function InfoField({ label, value }: { label: string; value?: string | null }) { + return ( + + + {label} + + + {value && value.trim() ? value : "—"} + + + ); +} + +export default function InvoiceDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [downloading, setDownloading] = useState(false); + + const { data: invoice, isLoading } = useQuery( + api.invoices.getById.queryOptions({ + input: { id: id ?? "" }, + enabled: Boolean(id), + }), + ); + + const downloadDocument = async () => { + if (!id) return; + setDownloading(true); + try { + const { data } = await invoicesService.downloadDocument(id); + openPdfBlob(data, `${invoice?.invoiceNumber ?? "invoice"}.pdf`); + } finally { + setDownloading(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!invoice) { + return ( + + + Invoice not found + + + + ); + } + + return ( + + } + action={ + void downloadDocument()} + > + + + } + /> + + + + + + Summary + + + + + + + + + + + + + + + + + + Line items + + + + + Description + Charge type + Quantity + Unit rate + Amount + + + + {(invoice.lines ?? []).map((line) => ( + + {line.description ?? line.chargeType} + + + {humanize(line.chargeType)} + + + {line.quantity} + + {formatMoney(line.unitRate, line.currency)} + + + {formatMoney(line.amount, line.currency)} + + + ))} + {(invoice.lines ?? []).length === 0 && ( + + + + No line items. + + + + )} + +
+ + + + + Subtotal + + + {formatMoney(invoice.subtotalAmount, invoice.currency)} + + + + + Tax + + + {formatMoney(invoice.taxAmount, invoice.currency)} + + + + + Paid + + + {formatMoney(invoice.paidAmount, invoice.currency)} + + + + + Total + + + {formatMoney(invoice.totalAmount, invoice.currency)} + + + +
+
+
+
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx new file mode 100644 index 000000000..7eca77ddf --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx @@ -0,0 +1,237 @@ +import type { Freight } from "@edr/types"; +import { + ActionIcon, + Box, + Card, + Group, + SegmentedControl, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; +import { useQuery } from "@tanstack/react-query"; +import { RefreshCw, Search, X } from "lucide-react"; +import { useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { + InvoiceStatusBadge, + formatDate, + formatMoney, + humanize, +} from "@/components/customers"; +import { PageContainer, PageHeader } from "@/components/page"; +import { api } from "@/services/api"; +import type { Invoice } from "@/types/invoice"; +import { + DataTable, + DataTableFooter, + usePagination, + type ColumnDef, +} from "@edr/ui-common"; + +export default function InvoicesPage() { + const navigate = useNavigate(); + const { pagination, setPagination } = usePagination({ pageSize: 10 }); + const [query, setQuery] = useState(""); + const [debouncedQuery] = useDebouncedValue(query, 300); + const [statusFilter, setStatusFilter] = useState<"" | Freight.InvoiceStatus>( + "", + ); + + const filter = useMemo( + () => ({ + page: pagination.pageIndex + 1, + pageSize: pagination.pageSize, + search: debouncedQuery, + status: statusFilter || undefined, + }), + [pagination.pageIndex, pagination.pageSize, debouncedQuery, statusFilter], + ); + + const { data, isLoading, isError, refetch, isFetching } = useQuery( + api.invoices.list.queryOptions({ input: { filter } }), + ); + + const rows = data?.items ?? []; + const total = data?.total ?? 0; + const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize)); + + const columns: ColumnDef[] = useMemo( + () => [ + { + id: "invoiceNumber", + header: "Invoice", + cell: ({ row }) => ( + + {row.original.invoiceNumber} + + ), + }, + { + id: "billedTo", + header: "Billed to", + cell: ({ row }) => ( + + {row.original.company?.name ?? "—"} + + ), + }, + { + id: "source", + header: "Source", + cell: ({ row }) => ( + + {humanize(row.original.source)} + + ), + }, + { + id: "status", + header: "Status", + cell: ({ row }) => , + }, + { + id: "amount", + header: "Amount", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + {formatMoney(row.original.totalAmount, row.original.currency)} + + ), + }, + { + id: "balance", + header: "Balance", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + {formatMoney(row.original.balanceAmount, row.original.currency)} + + ), + }, + { + id: "dueAt", + header: "Due", + meta: { headerClassName: "text-right", cellClassName: "text-right" }, + cell: ({ row }) => ( + + {formatDate(row.original.dueAt)} + + ), + }, + ], + [], + ); + + return ( + + void refetch()} + > + + + } + /> + + + + + + } + value={query} + onChange={(e) => setQuery(e.target.value)} + rightSection={ + query ? ( + setQuery("")} + > + + + ) : null + } + style={{ flex: 1, minWidth: "240px" }} + radius="lg" + /> + { + setStatusFilter( + v === "all" ? "" : (v as Freight.InvoiceStatus), + ); + setPagination((prev) => ({ ...prev, pageIndex: 0 })); + }} + data={[ + { label: "All", value: "all" }, + { label: "Pending", value: "PENDING" }, + { label: "Paid", value: "PAID" }, + { label: "Overdue", value: "OVERDUE" }, + ]} + /> + + {total} record{total !== 1 ? "s" : ""} + + + + + + + navigate(`/dashboard/invoices/${row.id}`)} + emptyMessage={ + debouncedQuery + ? "No invoices match your search." + : "No invoices yet." + } + error={ + isError + ? { + message: "Failed to load invoices.", + onRetry: () => void refetch(), + } + : undefined + } + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + manualPagination: true, + pageCount, + }} + containerClassName="border-0 shadow-none bg-transparent" + footer={DataTableFooter} + /> + + + + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index d7a60179c..fc8eb162c 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -28,6 +28,11 @@ import type { UpdateFileUploadFieldDto, UpdateFileUploadSettingDto, } from "@/types/fileUploadSettings"; +import type { + Invoice, + InvoiceListFilter, + PaginatedInvoices, +} from "@/types/invoice"; import type { IOverviewDashboard, OverviewRange } from "@/types/overview"; import { RuleEngineListResult, @@ -128,6 +133,7 @@ import { import { containerTypesService } from "./container-types.service"; import { containerService, type Container } from "./containerService"; import { customersService } from "./customers.service"; +import { invoicesService } from "./invoices.service"; import { dropdownSettingsService } from "./dropdownSettings.service"; import { fileUploadSettingsService } from "./fileUploadSettings.service"; import { @@ -197,7 +203,10 @@ const TRAIN_SCHEDULING_INVALIDATIONS: ReadonlyArray = [ export const api = { trainScheduling: { // ── Queries ──────────────────────────────────────────────────────────── - scheduleList: endpoint<{ freightType?: FreightType }, TrainScheduleListItem[]>( + scheduleList: endpoint< + { freightType?: FreightType }, + TrainScheduleListItem[] + >( "train-scheduling", "schedules", ({ freightType }) => trainSchedulingService.listSchedules(freightType), @@ -211,11 +220,16 @@ export const api = { () => QUERY_KEYS.TRAIN_SCHEDULING.batchBoard(), ), - batchBoardDetail: endpoint<{ scheduleId: string }, BatchBoardScheduleDetail>( + batchBoardDetail: endpoint< + { scheduleId: string }, + BatchBoardScheduleDetail + >( "train-scheduling", "batch-board-detail", - ({ scheduleId }) => trainSchedulingService.getBatchBoardDetail(scheduleId), - ({ scheduleId }) => QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(scheduleId), + ({ scheduleId }) => + trainSchedulingService.getBatchBoardDetail(scheduleId), + ({ scheduleId }) => + QUERY_KEYS.TRAIN_SCHEDULING.batchBoardDetail(scheduleId), ), scheduleDetail: endpoint< @@ -344,7 +358,10 @@ export const api = { ), // ── Mutations ────────────────────────────────────────────────────────── - runAllocation: endpoint<{ scheduleId: string }, WagonAllocationAttemptResult>( + runAllocation: endpoint< + { scheduleId: string }, + WagonAllocationAttemptResult + >( "train-scheduling", "run-allocation", ({ scheduleId }) => trainSchedulingService.runAllocation(scheduleId), @@ -454,7 +471,10 @@ export const api = { () => TRAIN_SCHEDULING_INVALIDATIONS, ), - pinWagons: endpoint<{ id: string; payload: PinWagonsPayload }, TrainScheduleDetail>( + pinWagons: endpoint< + { id: string; payload: PinWagonsPayload }, + TrainScheduleDetail + >( "train-scheduling", "pin-wagons", ({ id, payload }) => trainSchedulingService.pinWagons(id, payload), @@ -550,8 +570,10 @@ export const api = { ({ id }) => warehouseService.getById(id).then((r) => r.data), ), - dashboard: endpoint("warehouses", "dashboard", () => - warehouseService.dashboard().then((r) => r.data), + dashboard: endpoint( + "warehouses", + "dashboard", + () => warehouseService.dashboard().then((r) => r.data), ), create: endpoint( @@ -642,10 +664,8 @@ export const api = { listInventory: endpoint< { filter?: InventoryFilter }, WarehouseInventoryItem[] - >( - "warehouse-inventory", - "list", - ({ filter }) => warehouseService.listInventory(filter).then((r) => r.data), + >("warehouse-inventory", "list", ({ filter }) => + warehouseService.listInventory(filter).then((r) => r.data), ), inquiry: endpoint< @@ -658,11 +678,19 @@ export const api = { ({ filter }) => ["warehouse-inventory", "inquiry", filter], ), - eligibleBookings: endpoint<{ direction?: 'IMPORT' | 'EXPORT' } | void, EligibleBooking[]>( + eligibleBookings: endpoint< + { direction?: "IMPORT" | "EXPORT" } | void, + EligibleBooking[] + >( "warehouse-inventory", "eligible-bookings", - (input) => warehouseService.eligibleBookings(input?.direction).then((r) => r.data), - (input) => ["warehouse-inventory", "eligible-bookings", input?.direction ?? "ALL"], + (input) => + warehouseService.eligibleBookings(input?.direction).then((r) => r.data), + (input) => [ + "warehouse-inventory", + "eligible-bookings", + input?.direction ?? "ALL", + ], ), readyToLoadExport: endpoint( @@ -698,7 +726,11 @@ export const api = { "import-train-items", ({ scheduleId }) => warehouseService.importTrainItems(scheduleId).then((r) => r.data), - ({ scheduleId }) => ["warehouse-inventory", "import-train-items", scheduleId], + ({ scheduleId }) => [ + "warehouse-inventory", + "import-train-items", + scheduleId, + ], ), importUnloadedQueue: endpoint( @@ -766,8 +798,11 @@ export const api = { "inspection-reports", ({ inventoryId }) => warehouseService.listInspectionReports(inventoryId).then((r) => r.data), - ({ inventoryId }) => - ["warehouse-inventory", inventoryId, "inspection-reports"], + ({ inventoryId }) => [ + "warehouse-inventory", + inventoryId, + "inspection-reports", + ], ), allocationRules: endpoint( @@ -784,15 +819,28 @@ export const api = { () => ["warehouse-fee-rules"], ), - feePreview: endpoint<{ inventoryId: string; billingCurrency?: 'ETB' | 'USD' }, FeePreview[]>( + feePreview: endpoint< + { inventoryId: string; billingCurrency?: "ETB" | "USD" }, + FeePreview[] + >( "warehouse-inventory", "fee-preview", ({ inventoryId, billingCurrency }) => - warehouseService.feePreview(inventoryId, billingCurrency).then((r) => r.data), - ({ inventoryId, billingCurrency }) => ["warehouse-inventory", inventoryId, "fee-preview", billingCurrency ?? 'USD'], + warehouseService + .feePreview(inventoryId, billingCurrency) + .then((r) => r.data), + ({ inventoryId, billingCurrency }) => [ + "warehouse-inventory", + inventoryId, + "fee-preview", + billingCurrency ?? "USD", + ], ), - invoices: endpoint<{ filter?: WarehouseInvoiceFilter }, WarehouseFeeInvoice[]>( + invoices: endpoint< + { filter?: WarehouseInvoiceFilter }, + WarehouseFeeInvoice[] + >( "warehouse-fee-invoices", "list", ({ filter }) => warehouseService.listInvoices(filter).then((r) => r.data), @@ -821,7 +869,8 @@ export const api = { receiveInventory: endpoint( "warehouse-inventory", "receive", - (payload) => warehouseService.receiveInventory(payload).then((r) => r.data), + (payload) => + warehouseService.receiveInventory(payload).then((r) => r.data), undefined, () => [["warehouse-inventory"], ["warehouses"]], ), @@ -856,7 +905,8 @@ export const api = { >( "warehouse-inventory", "load", - ({ id, payload }) => warehouseService.load(id, payload).then((r) => r.data), + ({ id, payload }) => + warehouseService.load(id, payload).then((r) => r.data), undefined, () => INVENTORY_INVALIDATIONS, ), @@ -875,7 +925,8 @@ export const api = { >( "warehouse-inventory", "move", - ({ id, payload }) => warehouseService.move(id, payload).then((r) => r.data), + ({ id, payload }) => + warehouseService.move(id, payload).then((r) => r.data), undefined, () => INVENTORY_INVALIDATIONS, ), @@ -931,7 +982,8 @@ export const api = { bulkMarkInspected: endpoint( "warehouse-inventory", "bulk-mark-inspected", - (payload) => warehouseService.bulkMarkInspected(payload).then((r) => r.data), + (payload) => + warehouseService.bulkMarkInspected(payload).then((r) => r.data), undefined, () => INVENTORY_INVALIDATIONS, ), @@ -949,7 +1001,12 @@ export const api = { { scheduleId: string; warehouseId?: string; - assignments?: { bookingId: string; warehouseId: string; yardId: string; zoneId: string }[]; + assignments?: { + bookingId: string; + warehouseId: string; + yardId: string; + zoneId: string; + }[]; }, AutoUnloadArrivedResult >( @@ -1021,11 +1078,11 @@ export const api = { ), // ── Allocation + fee rules ───────────────────────────────────────────── - previewAllocation: endpoint( - "warehouse-allocation-rules", - "preview", - (criteria) => - warehouseService.previewAllocation(criteria).then((r) => r.data), + previewAllocation: endpoint< + AllocationCriteria, + AllocationPreviewResult | null + >("warehouse-allocation-rules", "preview", (criteria) => + warehouseService.previewAllocation(criteria).then((r) => r.data), ), createAllocationRule: endpoint( @@ -1087,7 +1144,11 @@ export const api = { // ── Invoices ─────────────────────────────────────────────────────────── generateInvoice: endpoint< - { inventoryId: string; confirmZero?: boolean; billingCurrency?: 'ETB' | 'USD' }, + { + inventoryId: string; + confirmZero?: boolean; + billingCurrency?: "ETB" | "USD"; + }, WarehouseFeeInvoice >( "warehouse-fee-invoices", @@ -1143,7 +1204,10 @@ export const api = { }, routes: { - list: endpoint<{ status?: import("./routes.service").RouteStatus } | void, RouteRecord[]>( + list: endpoint< + { status?: import("./routes.service").RouteStatus } | void, + RouteRecord[] + >( "routes", "list", (input) => @@ -1168,7 +1232,10 @@ export const api = { () => [["routes"]], ), - update: endpoint<{ id: string; data: Partial }, RouteRecord>( + update: endpoint< + { id: string; data: Partial }, + RouteRecord + >( "routes", "update", ({ id, data }) => routesService.update(id, data).then((r) => r.data), @@ -1281,10 +1348,8 @@ export const api = { ({ trainId }) => ["wagons", "train", trainId], ), - getById: endpoint<{ id: string }, Wagon>( - "wagons", - "getById", - ({ id }) => wagonService.getById(id).then((r) => r.data), + getById: endpoint<{ id: string }, Wagon>("wagons", "getById", ({ id }) => + wagonService.getById(id).then((r) => r.data), ), assignToTrain: endpoint< @@ -1663,7 +1728,8 @@ export const api = { >( "file-upload-settings", "addField", - ({ settingId, dto }) => fileUploadSettingsService.addField(settingId, dto), + ({ settingId, dto }) => + fileUploadSettingsService.addField(settingId, dto), undefined, () => [["file-upload-settings"]], ), @@ -1762,7 +1828,8 @@ export const api = { >( "dropdown-settings", "updateOption", - ({ optionId, dto }) => dropdownSettingsService.updateOption(optionId, dto), + ({ optionId, dto }) => + dropdownSettingsService.updateOption(optionId, dto), undefined, () => [["dropdown-settings"]], ), @@ -1815,11 +1882,10 @@ export const api = { ruleEngineService.update(resource, id, payload), ), - remove: endpoint< - { resource: RuleEngineResourceSlug; id: string }, - void - >("rule-engine", "remove", ({ resource, id }) => - ruleEngineService.remove(resource, id), + remove: endpoint<{ resource: RuleEngineResourceSlug; id: string }, void>( + "rule-engine", + "remove", + ({ resource, id }) => ruleEngineService.remove(resource, id), ), submitRate: endpoint<{ id: string }, RuleEngineRecord>( @@ -1842,14 +1908,21 @@ export const api = { ), reorder: endpoint< - { resource: RuleEngineResourceSlug; payload: { ids: string[]; requiresDirectorApproval?: boolean } }, + { + resource: RuleEngineResourceSlug; + payload: { ids: string[]; requiresDirectorApproval?: boolean }; + }, void >("rule-engine", "reorder", ({ resource, payload }) => ruleEngineService.reorder(resource, payload), ), moveOrder: endpoint< - { resource: RuleEngineResourceSlug; id: string; direction: "up" | "down" }, + { + resource: RuleEngineResourceSlug; + id: string; + direction: "up" | "down"; + }, void >("rule-engine", "moveOrder", ({ resource, id, direction }) => ruleEngineService.moveOrder(resource, id, direction), @@ -1871,10 +1944,8 @@ export const api = { ({ id }) => QUERY_KEYS.BOOKINGS.byId(id), ), - remove: endpoint<{ id: string }, void>( - "bookings", - "remove", - ({ id }) => bookingsService.remove(id), + remove: endpoint<{ id: string }, void>("bookings", "remove", ({ id }) => + bookingsService.remove(id), ), staffAccept: endpoint<{ id: string; validityDays: number }, BookingDetail>( @@ -1924,10 +1995,11 @@ export const api = { ({ id }) => bookingsService.generateContract(id), ), - getContractView: endpoint<{ id: string }, import("./bookings.service").ContractView>( - "bookings", - "getContractView", - ({ id }) => bookingsService.getContractView(id), + getContractView: endpoint< + { id: string }, + import("./bookings.service").ContractView + >("bookings", "getContractView", ({ id }) => + bookingsService.getContractView(id), ), signContract: endpoint< @@ -2011,7 +2083,8 @@ export const api = { >( "customers", "setProfileStatus", - ({ profileId, status }) => customersService.setProfileStatus(profileId, status), + ({ profileId, status }) => + customersService.setProfileStatus(profileId, status), undefined, (_input, data) => [ QUERY_KEYS.CUSTOMERS.byId(data.companyId), @@ -2032,6 +2105,22 @@ export const api = { ), }, + invoices: { + list: endpoint<{ filter: InvoiceListFilter }, PaginatedInvoices>( + "invoices", + "list", + ({ filter }) => invoicesService.list(filter), + ({ filter }) => QUERY_KEYS.INVOICES.list(filter), + ), + + getById: endpoint<{ id: string }, Invoice>( + "invoices", + "getById", + ({ id }) => invoicesService.getById(id), + ({ id }) => QUERY_KEYS.INVOICES.byId(id), + ), + }, + overview: { get: endpoint<{ range?: OverviewRange }, IOverviewDashboard>( "overview", diff --git a/apps/edr-freight-web/backoffice/src/services/invoices.service.ts b/apps/edr-freight-web/backoffice/src/services/invoices.service.ts new file mode 100644 index 000000000..a4abc6ea2 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/invoices.service.ts @@ -0,0 +1,36 @@ +import { api as apiClient } from "@/auth/http"; +import { URL_CONSTANTS } from "@/constants/URLS"; +import type { + Invoice, + InvoiceListFilter, + PaginatedInvoices, +} from "@/types/invoice"; + +const cleanParams = (params: object) => + Object.fromEntries( + Object.entries(params).filter( + ([, value]) => value !== undefined && value !== "" && value !== null, + ), + ); + +export const invoicesService = { + list(filter: InvoiceListFilter): Promise { + return apiClient + .get(URL_CONSTANTS.BILLING.INVOICES, { + params: cleanParams(filter), + }) + .then((r) => r.data); + }, + + getById(id: string): Promise { + return apiClient + .get(URL_CONSTANTS.BILLING.INVOICE_BY_ID(id)) + .then((r) => r.data); + }, + + downloadDocument(id: string) { + return apiClient.get(URL_CONSTANTS.BILLING.INVOICE_DOCUMENT(id), { + responseType: "blob", + }); + }, +}; diff --git a/apps/edr-freight-web/backoffice/src/types/invoice.ts b/apps/edr-freight-web/backoffice/src/types/invoice.ts new file mode 100644 index 000000000..26ecfcafa --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/types/invoice.ts @@ -0,0 +1,22 @@ +import type { Freight } from "@edr/types"; + +/** Mirrors backend `Invoice` (the shared `Freight.IInvoice` omits a couple of raw entity columns). */ +export interface Invoice extends Freight.IInvoice { + subtotalAmount: number; + taxAmount: number; +} + +/** Query parameters for the invoice list. */ +export interface InvoiceListFilter { + page: number; + pageSize: number; + companyId?: string; + status?: Freight.InvoiceStatus; + search?: string; +} + +/** Standard paginated list envelope (matches the customers/bookings service shape). */ +export interface PaginatedInvoices { + items: Invoice[]; + total: number; +}