From cdfe7c1f5b4c6602569b091190b357fe6b82c796 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Tue, 23 Jun 2026 15:02:25 +0300 Subject: [PATCH 1/3] fix: type errors --- apps/edr-freight-web/backoffice/package.json | 2 +- .../baselineRatematrix/RateMatrixForm.tsx | 330 -------- .../bookings/BookingRequestsHeader.tsx | 110 +-- .../bookings/detail/BookingRequestHero.tsx | 57 +- .../src/components/fleet/FleetFormDialog.tsx | 66 +- .../components/fleet/FleetRecordActions.tsx | 56 +- .../ruleEngine/RuleEngineCardGrid.tsx | 47 +- .../ruleEngine/RuleEngineListFooter.tsx | 2 +- .../warehouses/WarehouseInventoryTable.tsx | 141 ++-- .../src/components/warehouses/badges.tsx | 69 +- .../bookings/booking-actions.config.ts | 24 +- .../backoffice/src/lib/currentCustomer.ts | 45 - .../admin/rateMatrix/RateMatrixApproval.tsx | 117 --- .../rateMatrix/RateMatrixRegistration.tsx | 25 - .../src/pages/bookings/BookingDetailPage.tsx | 1 - .../bookings/BookingRequestDetailPage.tsx | 157 ++-- .../user-management/PositionTypesPage.tsx | 327 +++++--- .../dashboard/user-management/iamConfig.ts | 3 +- .../documents/EditFileUploadSettingDialog.tsx | 31 +- .../src/pages/payments/PaymentsPage.tsx | 43 +- .../ruleEngine/RuleEngineResourcePage.tsx | 124 ++- .../BatchScheduleDetailPage.tsx | 785 ++++++++++-------- .../services/ruleEngine/ruleEngine.service.ts | 42 +- .../src/services/trainScheduling.service.ts | 79 +- .../src/types/@tria-plc__iamui.d.ts | 55 ++ .../backoffice/src/types/trainScheduling.ts | 15 +- .../backoffice/tsconfig.app.json | 3 +- 27 files changed, 1379 insertions(+), 1377 deletions(-) delete mode 100644 apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx delete mode 100644 apps/edr-freight-web/backoffice/src/lib/currentCustomer.ts delete mode 100644 apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx delete mode 100644 apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx create mode 100644 apps/edr-freight-web/backoffice/src/types/@tria-plc__iamui.d.ts diff --git a/apps/edr-freight-web/backoffice/package.json b/apps/edr-freight-web/backoffice/package.json index e8c9d21e6..8f0e233b4 100644 --- a/apps/edr-freight-web/backoffice/package.json +++ b/apps/edr-freight-web/backoffice/package.json @@ -9,7 +9,7 @@ "preview": "vite preview --port 5183", "lint": "eslint src", "test": "vitest run", - "type-check": "tsc --noEmit" + "type-check": "tsc -b" }, "dependencies": { "@edr/types": "workspace:*", diff --git a/apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx b/apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx deleted file mode 100644 index f18389f9e..000000000 --- a/apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx +++ /dev/null @@ -1,330 +0,0 @@ -// components/baselineRatematrix/RateMatrixForm.tsx -import React, { useState, useCallback } from 'react'; -// import { useForm } from 'react-hook-form'; -// import { zodResolver } from '@hookform/resolvers/zod'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { toast } from 'sonner'; -import { z } from 'zod'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Loader2, Save, Send, Shield, AlertTriangle } from 'lucide-react'; -import { RateTypeSection } from './RateTypeSection'; -import { ConfirmationDialog } from './ConfirmationDialog'; -import { ValidationSummary } from './ValidationSummary'; -import { LoadingScreen } from '@/ui/LoadingScreen'; -import { useRateMatrixAuth } from '@/auth/hooks/useAuth'; -import { useReferenceData } from '@/hooks/useReferenceData'; -import { queryKeys } from '@/constants/queryKeys'; -import { API_URLS } from '@/constants/apiUrls'; -import { - RATE_TYPES, - RATE_TYPE_LABELS, - REQUIRED_RATE_TYPES -} from '@/constants/rateMatrixConstants'; -import { rateMatrixRulesEngine } from '../../ruleEngine/rateMatrixRules'; -import type { RateEntry } from './types'; - -const formSchema = z.object({ - matrixName: z.string().min(1, 'Matrix name is required').max(200), - effectiveDate: z.string().min(1, 'Effective date is required'), - expiryDate: z.string().optional(), - currency: z.string().min(1, 'Currency is required'), -}); - -type FormData = z.infer; - -const createInitialSections = (): RateEntry[] => { - return REQUIRED_RATE_TYPES.map(rateType => ({ - rateType, - entries: [{ - validFrom: '', - validTo: '', - }], - })); -}; - -export function RateMatrixForm() { - const [rateSections, setRateSections] = useState(createInitialSections()); - const [showConfirmation, setShowConfirmation] = useState(false); - const [savedMatrixId, setSavedMatrixId] = useState(null); - const [validationErrors, setValidationErrors] = useState([]); - - const { isDirector } = useRateMatrixAuth(); - const { data: referenceData, isLoading: isLoadingReference } = useReferenceData(); - const queryClient = useQueryClient(); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - matrixName: '', - effectiveDate: '', - expiryDate: '', - currency: 'USD', - }, - }); - - // Save draft mutation - const saveDraftMutation = useMutation({ - mutationFn: async (data: FormData & { rateSections: RateEntry[] }) => { - const response = await fetch(API_URLS.RATE_MATRIX.DRAFT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - if (!response.ok) throw new Error('Failed to save draft'); - return response.json(); - }, - onSuccess: (data) => { - setSavedMatrixId(data.id); - queryClient.invalidateQueries({ queryKey: queryKeys.rateMatrix.all }); - toast.success('Draft saved successfully'); - }, - onError: (error) => { - toast.error('Failed to save draft'); - }, - }); - - // Submit for approval mutation - const submitMutation = useMutation({ - mutationFn: async (matrixId: string) => { - const response = await fetch(API_URLS.RATE_MATRIX.SUBMIT(matrixId), { - method: 'POST', - }); - if (!response.ok) throw new Error('Failed to submit'); - return response.json(); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: queryKeys.rateMatrix.all }); - toast.success('Rate matrix submitted for executive approval and locked!'); - setShowConfirmation(false); - }, - onError: (error) => { - toast.error('Failed to submit for approval'); - setShowConfirmation(false); - }, - }); - - const handleValidate = useCallback(() => { - const validation = rateMatrixRulesEngine.validate(rateSections); - setValidationErrors([...validation.errors, ...validation.warnings]); - - if (validation.isValid) { - toast.success('All validations passed!'); - } - }, [rateSections]); - - const handleSaveDraft = async () => { - const formData = form.getValues(); - await saveDraftMutation.mutateAsync({ - ...formData, - rateSections, - }); - }; - - const handleSubmitClick = async () => { - const isFormValid = await form.trigger(); - if (!isFormValid) return; - - const validation = rateMatrixRulesEngine.validate(rateSections); - setValidationErrors([...validation.errors, ...validation.warnings]); - - if (!validation.isValid) { - toast.error('Please fix validation errors before submitting'); - return; - } - - setShowConfirmation(true); - }; - - const handleConfirmSubmit = async () => { - const formData = form.getValues(); - - try { - let matrixId = savedMatrixId; - - if (!matrixId) { - const draftResult = await saveDraftMutation.mutateAsync({ - ...formData, - rateSections, - }); - matrixId = draftResult.id; - } - - await submitMutation.mutateAsync(matrixId!); - } catch (error) { - // Error handling done in mutations - } - }; - - if (isLoadingReference) { - return ; - } - - if (!isDirector) { - return ( -
- - - Access Denied - - Only Directors can access the rate matrix registration. - - -
- ); - } - - return ( -
- {/* Header */} -
-

- Baseline Rate Matrix Registration -

-

- Submit a comprehensive rate matrix for executive approval -

-
- - {/* Director Warning */} - - - Director Notice - - Once submitted, this matrix will be locked pending Chief Executive approval. - No edits can be made by any user until authorization is granted. - - - -
e.preventDefault()}> - {/* Matrix Metadata */} - - - Matrix Information - - -
-
- - - {form.formState.errors.matrixName && ( -

- {form.formState.errors.matrixName.message} -

- )} -
- -
- - -
- -
- - -
- -
- - -
-
-
-
- - {/* Rate Type Sections */} -
- {rateSections.map((section, index) => ( - { - const newSections = [...rateSections]; - newSections[index] = updatedSection; - setRateSections(newSections); - }} - referenceData={referenceData} - /> - ))} -
- - {/* Validation Errors */} - {validationErrors.length > 0 && ( -
- -
- )} - - {/* Form Actions */} -
- - - - - -
-
- - {/* Confirmation Dialog */} - -
- ); -} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx index 4997f8ef8..88e522576 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingRequestsHeader.tsx @@ -21,20 +21,6 @@ import type { BookingListSummaryTabs, } from "@/services/bookings.service"; -/** Lifecycle stages for the pipeline distribution bar (in flow order). */ -const PIPELINE_STAGES: Array<{ - key: keyof BookingListSummaryTabs; - label: string; - color: string; -}> = [ - { key: "intake", label: "Intake", color: "#38bdf8" }, - { key: "in_approval", label: "Approval", color: "#f59e0b" }, - { key: "approved_contract", label: "Contract", color: "#8b5cf6" }, - { key: "payment", label: "Payment", color: "#fb923c" }, - { key: "operations", label: "Operations", color: "#14b8a6" }, - { key: "completed", label: "Completed", color: "#22c55e" }, -]; - const CARD_STYLE = { background: "var(--mantine-color-gray-0)", border: "1px solid var(--mantine-color-gray-2)", @@ -62,7 +48,12 @@ export function BookingRequestsHeader({ return ( - - @@ -285,4 +323,4 @@ const FleetFormDialog = ({ ); }; -export default FleetFormDialog; \ No newline at end of file +export default FleetFormDialog; diff --git a/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx b/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx index 3ec37d092..9096f52a1 100644 --- a/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx +++ b/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx @@ -1,5 +1,5 @@ import { Edit2, Trash2, Eye, Users, MoreVertical } from "lucide-react"; -import { ActionIcon, Group, Menu, MenuItem, Tooltip } from "@mantine/core"; +import { ActionIcon, Menu, MenuItem, Tooltip } from "@mantine/core"; import { useNavigate } from "react-router-dom"; import type { FleetResourceConfig } from "@/pages/fleet/config/resources"; @@ -37,30 +37,39 @@ const FleetRecordActions = ({ - + {isVehicle && onAssignDriver ? ( - onAssignDriver(record)} leftSection={}> + onAssignDriver(record)} + leftSection={} + > Assign Driver ) : null} - onEdit(record)} leftSection={}> + onEdit(record)} + leftSection={} + > Edit {showDetail ? ( - }> + } + > View details ) : null} - onRemove(record)} leftSection={}> + onRemove(record)} + leftSection={} + > {removeLabel} @@ -72,30 +81,39 @@ const FleetRecordActions = ({ - + {isVehicle && onAssignDriver ? ( - onAssignDriver(record)} leftSection={}> + onAssignDriver(record)} + leftSection={} + > Assign Driver ) : null} - onEdit(record)} leftSection={}> + onEdit(record)} + leftSection={} + > Edit {showDetail ? ( - }> + } + > View details ) : null} - onRemove(record)} leftSection={}> + onRemove(record)} + leftSection={} + > {removeLabel} diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx index 0310ca328..392bf6dfb 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineCardGrid.tsx @@ -1,4 +1,4 @@ -import type { OnChangeFn, PaginationState } from "@tanstack/react-table"; +import type { OnChangeFn, PaginationState } from "@edr/ui-common"; import { Stack, Group, Text, Card, SimpleGrid, Skeleton } from "@mantine/core"; import type { RuleEngineResourceConfig } from "@/pages/ruleEngine/config/resources"; @@ -85,8 +85,15 @@ const RuleEngineCardGrid = ({ if (status === "error") { return ( - - Failed to load data + + + Failed to load data + Please refresh the page or try again later. @@ -118,8 +125,15 @@ const RuleEngineCardGrid = ({ if (status === "success" && rows.length === 0) { return ( - - {emptyMessage} + + + {emptyMessage} + Try adjusting your search or add a new record. @@ -197,7 +211,10 @@ const RuleEngineCardGrid = ({ {subtitle && ( - {presentation.subtitleKey === "stepOrder" ? "Step" : "Type"}: + {presentation.subtitleKey === "stepOrder" + ? "Step" + : "Type"} + : {subtitle} @@ -207,7 +224,12 @@ const RuleEngineCardGrid = ({ {presentation.detailColumns.map((col) => { const displayValue = getSmartValue(record, col.accessorKey); return ( - + {col.header}: @@ -220,14 +242,19 @@ const RuleEngineCardGrid = ({ )} - + {})} - onDelete={onDelete ?? (() => {})} + onEdit={onEdit ?? (() => { })} + onDelete={onDelete ?? (() => { })} onViewChain={onViewChain} onSubmitRate={onSubmitRate} onApproveRate={onApproveRate} diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineListFooter.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineListFooter.tsx index ea8087bd2..f53d6cb57 100644 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineListFooter.tsx +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/RuleEngineListFooter.tsx @@ -1,4 +1,4 @@ -import type { OnChangeFn, PaginationState } from "@tanstack/react-table"; +import type { OnChangeFn, PaginationState } from "@edr/ui-common"; import { Group, Pagination, Select, Text } from "@mantine/core"; export interface RuleEngineListFooterProps { diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx index a1a00d2e9..ec6f0f7d0 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx @@ -1,12 +1,14 @@ -import { useMemo } from 'react'; -import { ActionIcon, Badge, Button, Group, Text, Tooltip } from '@mantine/core'; -import { ArrowRightLeft, ClipboardList, Coins, History } from 'lucide-react'; -import { DataTable, type ColumnDef } from '@edr/ui-common'; +import { useMemo } from "react"; +import { ActionIcon, Badge, Button, Group, Text, Tooltip } from "@mantine/core"; +import { ArrowRightLeft, ClipboardList, Coins, History } from "lucide-react"; +import { DataTable, type ColumnDef } from "@edr/ui-common"; -import type { InventoryAction, WarehouseInventoryItem } from '@/types/warehouse'; -import { getNextInventoryAction } from '@/types/warehouse'; -import { InventoryStatusBadge } from './badges'; -import { formatDate, formatNumber, humanizeEnum } from './options'; +import type { + InventoryAction, + WarehouseInventoryItem, +} from "@/types/warehouse"; +import { InventoryStatusBadge } from "./badges"; +import { formatDate, formatNumber, humanizeEnum } from "./options"; interface WarehouseInventoryTableProps { items: WarehouseInventoryItem[]; @@ -27,21 +29,21 @@ interface WarehouseInventoryTableProps { } const itemKind = (item: WarehouseInventoryItem) => { - if (item.containerId) return { label: 'Container', color: 'blue' }; - if (item.cargoId) return { label: 'Cargo', color: 'grape' }; - if (item.goodsId) return { label: 'Goods', color: 'orange' }; - return { label: '—', color: 'gray' }; + if (item.containerId) return { label: "Container", color: "blue" }; + if (item.cargoId) return { label: "Cargo", color: "grape" }; + if (item.goodsId) return { label: "Goods", color: "orange" }; + return { label: "—", color: "gray" }; }; const actionColor: Record = { - store: 'blue', - reserve: 'grape', - 'ready-for-loading': 'cyan', - load: 'teal', - dispatch: 'edr-green', - 'ready-for-pickup': 'orange', - release: 'yellow', - deliver: 'green', + store: "blue", + reserve: "grape", + "ready-for-loading": "cyan", + load: "teal", + dispatch: "edr-green", + "ready-for-pickup": "orange", + release: "yellow", + deliver: "green", }; export function WarehouseInventoryTable({ @@ -62,8 +64,8 @@ export function WarehouseInventoryTable({ const columns = useMemo[]>( () => [ { - id: 'booking', - header: 'Booking', + id: "booking", + header: "Booking", cell: ({ row }) => row.original.bookingId ? ( @@ -78,16 +80,28 @@ export function WarehouseInventoryTable({ ), }, { - id: 'facility', - header: 'Facility', - cell: ({ row }) => row.original.warehouse?.facility?.name ?? '—', + id: "facility", + header: "Facility", + cell: ({ row }) => row.original.warehouse?.facility?.name ?? "—", }, - { id: 'warehouse', header: 'Warehouse', cell: ({ row }) => row.original.warehouse?.code ?? '—' }, - { id: 'yard', header: 'Yard', cell: ({ row }) => row.original.yard?.code ?? '—' }, - { id: 'zone', header: 'Zone', cell: ({ row }) => row.original.zone?.code ?? '—' }, { - id: 'item', - header: 'Item', + id: "warehouse", + header: "Warehouse", + cell: ({ row }) => row.original.warehouse?.code ?? "—", + }, + { + id: "yard", + header: "Yard", + cell: ({ row }) => row.original.yard?.code ?? "—", + }, + { + id: "zone", + header: "Zone", + cell: ({ row }) => row.original.zone?.code ?? "—", + }, + { + id: "item", + header: "Item", cell: ({ row }) => { const kind = itemKind(row.original); return ( @@ -97,27 +111,44 @@ export function WarehouseInventoryTable({ ); }, }, - { id: 'qty', header: 'Qty', cell: ({ row }) => formatNumber(row.original.quantity) }, - { id: 'weight', header: 'Weight', cell: ({ row }) => formatNumber(row.original.weight) }, { - id: 'status', - header: 'Status', - cell: ({ row }) => , + id: "qty", + header: "Qty", + cell: ({ row }) => formatNumber(row.original.quantity), }, { - id: 'arrived', - header: 'Arrived', - cell: ({ row }) => {formatDate(row.original.arrivedAt)}, + id: "weight", + header: "Weight", + cell: ({ row }) => formatNumber(row.original.weight), }, { - id: 'actions', - header: '', + id: "status", + header: "Status", + cell: ({ row }) => ( + + ), + }, + { + id: "arrived", + header: "Arrived", + cell: ({ row }) => ( + {formatDate(row.original.arrivedAt)} + ), + }, + { + id: "actions", + header: "", cell: ({ row }) => { const item = row.original; const busy = busyId === item.id; const nextAction = INVENTORY_NEXT_ACTION[item.status]; return ( - e.stopPropagation()}> + e.stopPropagation()} + > {nextAction && ( )} - {item.status !== 'DISPATCHED' && ( + {item.status !== "DISPATCHED" && ( - onMove(item)}> + onMove(item)} + > )} {onInspect && ( - onInspect(item)}> + onInspect(item)} + > )} {onFeePreview && ( - onFeePreview(item)}> + onFeePreview(item)} + > )} - onHistory(item)}> + onHistory(item)} + > diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/badges.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/badges.tsx index 1808a78bb..aebbd4de4 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/badges.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/badges.tsx @@ -1,53 +1,80 @@ -import { Badge } from '@mantine/core'; +import { Badge } from "@mantine/core"; -import type { InventoryStatus, WarehouseStatus, WarehouseType } from '@/types/warehouse'; +import type { + InventoryStatus, + WarehouseStatus, + WarehouseType, +} from "@/types/warehouse"; const humanize = (value: string) => value .toLowerCase() - .split('_') + .split("_") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(' '); + .join(" "); const badgeStyle = { - fontSize: '0.7rem', - letterSpacing: '0.04em', - whiteSpace: 'nowrap' as const, + fontSize: "0.7rem", + letterSpacing: "0.04em", + whiteSpace: "nowrap" as const, }; export function WarehouseTypeBadge({ type }: { type: WarehouseType }) { - const color = type === 'CLOSED_WAREHOUSE' ? 'indigo' : 'teal'; + const color = type === "CLOSED_WAREHOUSE" ? "indigo" : "teal"; return ( - + {humanize(type)} ); } export function WarehouseStatusBadge({ status }: { status: WarehouseStatus }) { - const color = status === 'ACTIVE' ? 'edr-green' : 'gray'; + const color = status === "ACTIVE" ? "edr-green" : "gray"; return ( - + {status} ); } const inventoryStatusColor: Record = { - UNLOADED: 'indigo', - RECEIVED: 'yellow', - STORED: 'blue', - RESERVED: 'grape', - READY_FOR_LOADING: 'cyan', - LOADED: 'teal', - DISPATCHED: 'edr-green', - DELIVERED: 'edr-green', + UNLOADED: "indigo", + RECEIVED: "yellow", + STORED: "blue", + RESERVED: "grape", + READY_FOR_LOADING: "cyan", + LOADED: "teal", + READY_FOR_PICKUP: "teal", + DISPATCHED: "edr-green", + DELIVERED: "edr-green", }; export function InventoryStatusBadge({ status }: { status: InventoryStatus }) { - const color = inventoryStatusColor[status] ?? 'gray'; + const color = inventoryStatusColor[status] ?? "gray"; return ( - + {humanize(status)} ); diff --git a/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts b/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts index cdb0d3d83..b71ad779c 100644 --- a/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts +++ b/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts @@ -55,7 +55,11 @@ export interface BookingActionDef { export type BookingActionContext = Pick< BookingDetail, - "status" | "paymentCurrency" | "approvalSteps" | "reference" | "schedulingStatus" + | "status" + | "paymentCurrency" + | "approvalSteps" + | "reference" + | "schedulingStatus" >; const ALLOCATABLE_SCHEDULING_STATUSES = new Set([ @@ -67,7 +71,9 @@ const ALLOCATABLE_SCHEDULING_STATUSES = new Set([ "", ]); -export function canAllocateBooking(booking: Pick) { +export function canAllocateBooking( + booking: Pick, +) { return ( booking.status === "PAID" && ALLOCATABLE_SCHEDULING_STATUSES.has(booking.schedulingStatus ?? undefined) @@ -286,10 +292,18 @@ export function getBookingActions( actions = [SIGN_CONTRACT_STAFF_ACTION, VIEW_CONTRACT_ACTION]; break; case "FULLY_EXECUTED": - actions = [{ ...VIEW_CONTRACT_ACTION, label: "View executed contract", primary: true }]; + actions = [ + { + ...VIEW_CONTRACT_ACTION, + label: "View executed contract", + primary: true, + }, + ]; break; case "PAID": - if (canAllocateBooking({ status, schedulingStatus: ctx.schedulingStatus })) { + if ( + canAllocateBooking({ status, schedulingStatus: ctx.schedulingStatus }) + ) { actions = [ { id: "allocateBooking", @@ -384,7 +398,7 @@ export function listRowHasActions( paymentCurrency: row.paymentCurrency, reference: "", approvalSteps: row.approvalSteps ?? undefined, - schedulingStatus: row.schedulingStatus, + schedulingStatus: row.status, }, user, ); diff --git a/apps/edr-freight-web/backoffice/src/lib/currentCustomer.ts b/apps/edr-freight-web/backoffice/src/lib/currentCustomer.ts deleted file mode 100644 index 0a68cd4f5..000000000 --- a/apps/edr-freight-web/backoffice/src/lib/currentCustomer.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { customers, type Customer } from "@/pages/customers/customers.mock"; -import { bookings, type Booking } from "@/pages/bookings/bookings.mock"; -import { - consignments, - type Consignment, -} from "@/pages/consignments/consignments.mock"; -import { - shipments, - type Shipment, -} from "@/pages/tracking/shipments.mock"; -import { invoices, type Invoice } from "@/pages/billing/invoices.mock"; - -/** - * Mock "logged-in customer". When auth integrates, replace this with the value - * pulled from `@edr/iamui-common` / the JWT context. - */ -const CURRENT_CUSTOMER_ID = 1; - -export function getCurrentCustomer(): Customer { - return ( - customers.find((c) => c.id === CURRENT_CUSTOMER_ID) ?? - (customers[0] as Customer) - ); -} - -export function getMyBookings(): Booking[] { - const me = getCurrentCustomer(); - return bookings.filter((b) => b.customerId === me.id); -} - -export function getMyConsignments(): Consignment[] { - const me = getCurrentCustomer(); - const myBookingIds = new Set(getMyBookings().map((b) => b.id)); - return consignments.filter((c) => myBookingIds.has(c.bookingId)); -} - -export function getMyShipments(): Shipment[] { - const myBookingIds = new Set(getMyBookings().map((b) => b.id)); - return shipments.filter((s) => myBookingIds.has(s.bookingId)); -} - -export function getMyInvoices(): Invoice[] { - const me = getCurrentCustomer(); - return invoices.filter((inv) => inv.customerId === me.id); -} diff --git a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx deleted file mode 100644 index 723821936..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx +++ /dev/null @@ -1,117 +0,0 @@ -// pages/admin/rateMatrix/RateMatrixApproval.tsx -import React from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { LoadingScreen } from '@/ui/LoadingScreen'; -import { useRateMatrixAuth } from '@/auth/hooks/useAuth'; -import { queryKeys } from '../../../constants/QUERY_KEYS'; -import { API_URLS } from '@/constants/URL_CONSTANTS'; -//import { MATRIX_STATUS } from '@/constants/rateMatrixConstants'; -import { toast } from 'sonner'; -import { Navigate } from 'react-router-dom'; - -export default function RateMatrixApprovalPage() { - const { isChiefExecutive } = useRateMatrixAuth(); - const queryClient = useQueryClient(); - const pendingMatricesQueryKey = [...queryKeys.rateMatrix.all, 'pending-approval']; - - const { data: pendingMatrices, isLoading } = useQuery({ - queryKey: pendingMatricesQueryKey, - queryFn: async () => { - const response = await fetch(`${API_URLS.RATE_MATRIX.LIST}?status=pending_approval`); - return response.json(); - }, - }); - - const authorizeMutation = useMutation({ - mutationFn: async ({ matrixId, signature }: { matrixId: string; signature: string }) => { - const response = await fetch(API_URLS.RATE_MATRIX.AUTHORIZE(matrixId), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ digitalSignature: signature }), - }); - if (!response.ok) throw new Error('Authorization failed'); - return response.json(); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: pendingMatricesQueryKey }); - toast.success('Rate matrix authorized successfully!'); - }, - onError: () => { - toast.error('Failed to authorize rate matrix'); - }, - }); - - if (!isChiefExecutive) { - return ; - } - - if (isLoading) return ; - - return ( -
-

Pending Rate Matrix Approvals

- -
- {pendingMatrices?.map((matrix: any) => ( - - - - {matrix.matrixName} - {matrix.status} - - - -
-
-
-

Effective Date

-

{matrix.effectiveDate}

-
-
-

Submitted By

-

{matrix.createdBy}

-
-
- -
-

Rate Types Included:

-
- {matrix.rateEntries?.map((entry: any) => ( - - {entry.rateType} - - ))} -
-
- -
- - -
-
-
-
- ))} -
-
- ); -} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx deleted file mode 100644 index 6f151f992..000000000 --- a/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// pages/admin/rateMatrix/RateMatrixRegistration.tsx -import React from 'react'; -import { RateMatrixForm } from '@/components/baselineRatematrix/RateMatrixForm'; -import { useRateMatrixAuth } from '@/auth/useAuth'; -import { Navigate } from 'react-router-dom'; -// Local lightweight fallback for LoadingScreen to avoid import errors -const LoadingScreen: React.FC<{ message?: string }> = ({ message = 'Loading...' }) => ( -
-
{message}
-
-); - -export default function RateMatrixRegistrationPage() { - const { isDirector, isLoading } = useRateMatrixAuth(); - - if (isLoading) { - return ; - } - - if (!isDirector) { - return ; - } - - return ; -} \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx index 481eeb407..0497f0ce8 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx @@ -104,7 +104,6 @@ const BookingDetailPage = () => { const approvedCount = approvalSteps.filter( (s) => s.status === "APPROVED", ).length; - const totalSteps = approvalSteps.length; return (
diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx index 2e5b9ee0f..901226436 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx @@ -34,7 +34,10 @@ import { WarehouseInfoCard } from "@/components/warehouses"; import { getStatusMeta } from "@/features/bookings/booking-status.config"; import { toBookingListRow } from "@/features/bookings/mapBookingListRow"; import { downloadBookingFile } from "@/services/files.service"; -import { useBookingDetail, useBookingMutations } from "@/hooks/bookings/useBookings"; +import { + useBookingDetail, + useBookingMutations, +} from "@/hooks/bookings/useBookings"; import toast from "react-hot-toast"; // Signature / generated-contract files are surfaced on the contract page, not @@ -49,7 +52,13 @@ const SIGNATURE_FILE_CODES = new Set([ export default function BookingRequestDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { data: booking, isLoading, isError, refetch, isFetching } = useBookingDetail(id); + const { + data: booking, + isLoading, + isError, + refetch, + isFetching, + } = useBookingDetail(id); const mutations = useBookingMutations(id ?? ""); const handleDownloadFile = async (file: BookingFileView) => { @@ -79,7 +88,13 @@ export default function BookingRequestDetailPage() { return ( - +
- navigate("/dashboard/booking-requests")} - onRefresh={() => refetch()} - isFetching={isFetching} - /> + navigate("/dashboard/booking-requests")} + onRefresh={() => refetch()} + isFetching={isFetching} + /> - + - {booking.status === "PENDING_CONSOLIDATION" && ( - - )} + {booking.status === "PENDING_CONSOLIDATION" && ( + + )} - - {/* LEFT — primary content */} - - - - - - {booking.contractSummary && ( - + + {/* LEFT — primary content */} + + + + + + {booking.contractSummary && ( + + )} + !SIGNATURE_FILE_CODES.has(f.code ?? ""), )} - !SIGNATURE_FILE_CODES.has(f.code ?? ""), - )} - onDownload={handleDownloadFile} - /> - - + onDownload={handleDownloadFile} + /> + + - {/* RIGHT — sticky action / summary rail */} - - - - - - - - {showContractButton && ( - - )} - {showApprovalCard && ( - - )} - - - - + {/* RIGHT — sticky action / summary rail */} + + + + + + + + {showContractButton && ( + + )} + {showApprovalCard && ( + + )} + + + + ); diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx index 9895ae36a..55a4df00e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/PositionTypesPage.tsx @@ -140,30 +140,38 @@ const PositionTypesPage = () => { const [loadingOrganizations, setLoadingOrganizations] = useState(true); const [loadingUnits, setLoadingUnits] = useState(false); const [loadingPositionTypes, setLoadingPositionTypes] = useState(false); - const [loadingPermissionsCatalog, setLoadingPermissionsCatalog] = useState(false); + const [loadingPermissionsCatalog, setLoadingPermissionsCatalog] = + useState(false); const [submitting, setSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); - const [selectedPositionType, setSelectedPositionType] = useState(null); - const [positionTypePermissions, setPositionTypePermissions] = useState([]); + const [selectedPositionType, setSelectedPositionType] = + useState(null); const [allPermissions, setAllPermissions] = useState([]); const [permissionsLoading, setPermissionsLoading] = useState(false); const [permissionsError, setPermissionsError] = useState(null); const [permissionSearch, setPermissionSearch] = useState(""); - const [selectedPermissionIds, setSelectedPermissionIds] = useState([]); + const [selectedPermissionIds, setSelectedPermissionIds] = useState( + [], + ); const [isCreateOpen, setIsCreateOpen] = useState(false); const [createForm, setCreateForm] = useState(emptyCreateForm); const [createPermissionSearch, setCreatePermissionSearch] = useState(""); const [createPermissionIds, setCreatePermissionIds] = useState([]); const [createError, setCreateError] = useState(null); - const [editForm, setEditForm] = useState(emptyEditForm); + const [editForm, setEditForm] = + useState(emptyEditForm); - const isSuperAdmin = Boolean(user?.roles?.some((role) => role.key === "super_admin")); + const isSuperAdmin = Boolean( + user?.roles?.some((role) => role.key === "super_admin"), + ); const allowedOrgIds = useMemo( () => new Set( (user?.employee ?? []) .map((employee) => employee.organizationId) - .filter((organizationId): organizationId is string => Boolean(organizationId)), + .filter((organizationId): organizationId is string => + Boolean(organizationId), + ), ), [user?.employee], ); @@ -173,14 +181,21 @@ const PositionTypesPage = () => { return organizations; } - return organizations.filter((organization) => allowedOrgIds.has(organization.id)); + return organizations.filter((organization) => + allowedOrgIds.has(organization.id), + ); }, [allowedOrgIds, isSuperAdmin, organizations]); const selectedOrganization = - visibleOrganizations.find((organization) => organization.id === selectedOrgId) ?? null; + visibleOrganizations.find( + (organization) => organization.id === selectedOrgId, + ) ?? null; const selectedUnit = units.find((unit) => unit.id === selectedUnitId) ?? null; const availableCopySources = useMemo( - () => positionTypes.filter((positionType) => positionType.id !== selectedPositionType?.id), + () => + positionTypes.filter( + (positionType) => positionType.id !== selectedPositionType?.id, + ), [positionTypes, selectedPositionType?.id], ); const filteredPermissions = useMemo(() => { @@ -191,8 +206,13 @@ const PositionTypesPage = () => { return true; } - const label = getLocaleLabel(permission.name, permission.key).toLowerCase(); - return label.includes(query) || permission.key.toLowerCase().includes(query); + const label = getLocaleLabel( + permission.name, + permission.key, + ).toLowerCase(); + return ( + label.includes(query) || permission.key.toLowerCase().includes(query) + ); }); }, [allPermissions, permissionSearch]); const filteredCreatePermissions = useMemo(() => { @@ -203,18 +223,29 @@ const PositionTypesPage = () => { return true; } - const label = getLocaleLabel(permission.name, permission.key).toLowerCase(); - return label.includes(query) || permission.key.toLowerCase().includes(query); + const label = getLocaleLabel( + permission.name, + permission.key, + ).toLowerCase(); + return ( + label.includes(query) || permission.key.toLowerCase().includes(query) + ); }); }, [allPermissions, createPermissionSearch]); - const allFilteredPermissionIds = filteredPermissions.map((permission) => permission.id); - const allFilteredCreatePermissionIds = filteredCreatePermissions.map((permission) => permission.id); + const allFilteredPermissionIds = filteredPermissions.map( + (permission) => permission.id, + ); + const allFilteredCreatePermissionIds = filteredCreatePermissions.map( + (permission) => permission.id, + ); const areAllFilteredPermissionsSelected = allFilteredPermissionIds.length > 0 && allFilteredPermissionIds.every((id) => selectedPermissionIds.includes(id)); const areAllFilteredCreatePermissionsSelected = allFilteredCreatePermissionIds.length > 0 && - allFilteredCreatePermissionIds.every((id) => createPermissionIds.includes(id)); + allFilteredCreatePermissionIds.every((id) => + createPermissionIds.includes(id), + ); const loadPositionTypes = async (unitId: string) => { const response = await api.get>( @@ -247,7 +278,8 @@ const PositionTypesPage = () => { setErrorMessage(null); try { - const response = await api.get>("/organizations"); + const response = + await api.get>("/organizations"); if (!isMounted) { return; @@ -261,7 +293,7 @@ const PositionTypesPage = () => { setErrorMessage( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load organizations." + ? (error.response?.data?.message ?? "Unable to load organizations.") : "Unable to load organizations.", ); } finally { @@ -285,12 +317,15 @@ const PositionTypesPage = () => { setLoadingPermissionsCatalog(true); try { - const response = await api.get>("/permissions", { - params: { - skip: 0, - take: 2000, + const response = await api.get>( + "/permissions", + { + params: { + skip: 0, + take: 2000, + }, }, - }); + ); if (!isMounted) { return; @@ -326,7 +361,12 @@ const PositionTypesPage = () => { return; } - if (selectedOrgId && visibleOrganizations.some((organization) => organization.id === selectedOrgId)) { + if ( + selectedOrgId && + visibleOrganizations.some( + (organization) => organization.id === selectedOrgId, + ) + ) { return; } @@ -350,7 +390,9 @@ const PositionTypesPage = () => { setPositionTypes([]); try { - const response = await api.get>(`/units/list/${selectedOrgId}`); + const response = await api.get>( + `/units/list/${selectedOrgId}`, + ); const items = getItems(response.data); if (!isMounted) { @@ -367,7 +409,7 @@ const PositionTypesPage = () => { setUnits([]); setErrorMessage( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load units." + ? (error.response?.data?.message ?? "Unable to load units.") : "Unable to load units.", ); } finally { @@ -412,7 +454,8 @@ const PositionTypesPage = () => { setPositionTypes([]); setErrorMessage( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load position types." + ? (error.response?.data?.message ?? + "Unable to load position types.") : "Unable to load position types.", ); } finally { @@ -431,7 +474,6 @@ const PositionTypesPage = () => { useEffect(() => { if (!selectedPositionType) { - setPositionTypePermissions([]); setSelectedPermissionIds([]); setEditForm(emptyEditForm); setPermissionsError(null); @@ -453,23 +495,24 @@ const PositionTypesPage = () => { setPermissionsError(null); try { - const items = await loadPermissionsForPositionType(selectedPositionType.id); + const items = await loadPermissionsForPositionType( + selectedPositionType.id, + ); if (!isMounted) { return; } - setPositionTypePermissions(items); setSelectedPermissionIds(items.map((permission) => permission.id)); } catch (error) { if (!isMounted) { return; } - setPositionTypePermissions([]); setPermissionsError( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to load position type permissions." + ? (error.response?.data?.message ?? + "Unable to load position type permissions.") : "Unable to load position type permissions.", ); } finally { @@ -499,13 +542,16 @@ const PositionTypesPage = () => { setPositionTypes(items); if (selectedPositionType) { - const nextSelected = items.find((item) => item.id === selectedPositionType.id) ?? selectedPositionType; + const nextSelected = + items.find((item) => item.id === selectedPositionType.id) ?? + selectedPositionType; setSelectedPositionType(nextSelected); } } catch (error) { setErrorMessage( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to refresh position types." + ? (error.response?.data?.message ?? + "Unable to refresh position types.") : "Unable to refresh position types.", ); } finally { @@ -522,7 +568,10 @@ const PositionTypesPage = () => { }; const handleSelectCopySource = async (positionTypeId: string) => { - setCreateForm((current) => ({ ...current, copyPermissionFromId: positionTypeId })); + setCreateForm((current) => ({ + ...current, + copyPermissionFromId: positionTypeId, + })); if (!positionTypeId) { setCreatePermissionIds([]); @@ -530,18 +579,23 @@ const PositionTypesPage = () => { } try { - const copiedPermissions = await loadPermissionsForPositionType(positionTypeId); - setCreatePermissionIds(copiedPermissions.map((permission) => permission.id)); + const copiedPermissions = + await loadPermissionsForPositionType(positionTypeId); + setCreatePermissionIds( + copiedPermissions.map((permission) => permission.id), + ); } catch (error) { setCreateError( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to copy permissions." + ? (error.response?.data?.message ?? "Unable to copy permissions.") : "Unable to copy permissions.", ); } }; - const handleCreatePositionType = async (event: React.FormEvent) => { + const handleCreatePositionType = async ( + event: React.FormEvent, + ) => { event.preventDefault(); if (!selectedUnitId) { @@ -576,7 +630,7 @@ const PositionTypesPage = () => { } catch (error) { setCreateError( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to create position type." + ? (error.response?.data?.message ?? "Unable to create position type.") : "Unable to create position type.", ); } finally { @@ -611,21 +665,26 @@ const PositionTypesPage = () => { const [refreshedPermissions, refreshedPositionTypes] = await Promise.all([ loadPermissionsForPositionType(selectedPositionType.id), - selectedUnitId ? loadPositionTypes(selectedUnitId) : Promise.resolve(positionTypes), + selectedUnitId + ? loadPositionTypes(selectedUnitId) + : Promise.resolve(positionTypes), ]); - setPositionTypePermissions(refreshedPermissions); - setSelectedPermissionIds(refreshedPermissions.map((permission) => permission.id)); + setSelectedPermissionIds( + refreshedPermissions.map((permission) => permission.id), + ); setPositionTypes(refreshedPositionTypes); - const refreshedSelected = refreshedPositionTypes.find((item) => item.id === selectedPositionType.id); + const refreshedSelected = refreshedPositionTypes.find( + (item) => item.id === selectedPositionType.id, + ); if (refreshedSelected) { setSelectedPositionType(refreshedSelected); } } catch (error) { setPermissionsError( isAxiosError(error) - ? error.response?.data?.message ?? "Unable to update position type." + ? (error.response?.data?.message ?? "Unable to update position type.") : "Unable to update position type.", ); } finally { @@ -633,34 +692,6 @@ const PositionTypesPage = () => { } }; - const handleSavePermissions = async () => { - if (!selectedPositionType) { - return; - } - - setSubmitting(true); - setPermissionsError(null); - - try { - await api.post("/position-type-permissions/assign-seconds-for-first", { - firstId: selectedPositionType.id, - secondIds: selectedPermissionIds, - }); - - const items = await loadPermissionsForPositionType(selectedPositionType.id); - setPositionTypePermissions(items); - setSelectedPermissionIds(items.map((permission) => permission.id)); - } catch (error) { - setPermissionsError( - isAxiosError(error) - ? error.response?.data?.message ?? "Unable to update position type permissions." - : "Unable to update position type permissions.", - ); - } finally { - setSubmitting(false); - } - }; - return (
@@ -670,9 +701,12 @@ const PositionTypesPage = () => {

-

Position Type

+

+ Position Type +

- Browse position types for a selected organization unit, add new ones, and manage their permissions. + Browse position types for a selected organization unit, add new + ones, and manage their permissions.

@@ -712,7 +746,13 @@ const PositionTypesPage = () => { disabled={loadingOrganizations || !visibleOrganizations.length} > - + {visibleOrganizations.map((organization) => ( @@ -734,7 +774,11 @@ const PositionTypesPage = () => { disabled={!selectedOrgId || loadingUnits || !units.length} > - + {units.map((unit) => ( @@ -800,7 +844,9 @@ const PositionTypesPage = () => { {getLocaleLabel(positionType.name, positionType.key)} - {positionType.key} + + {positionType.key} + {positionType.isSystem ? "System" : "Unit"} @@ -833,7 +879,10 @@ const PositionTypesPage = () => { {selectedPositionType - ? getLocaleLabel(selectedPositionType.name, selectedPositionType.key) + ? getLocaleLabel( + selectedPositionType.name, + selectedPositionType.key, + ) : "Position type details"} @@ -851,7 +900,10 @@ const PositionTypesPage = () => { Position type

- {getLocaleLabel(selectedPositionType.name, selectedPositionType.key)} + {getLocaleLabel( + selectedPositionType.name, + selectedPositionType.key, + )}

@@ -883,23 +935,33 @@ const PositionTypesPage = () => {
{selectedPositionType.isSystem ? (

- System position types keep their name and key, but you can still manage permissions here. + System position types keep their name and key, but you can + still manage permissions here.

) : null}
-

Permissions

+

+ Permissions +

{selectedPermissionIds.length} permissions selected
@@ -942,7 +1010,9 @@ const PositionTypesPage = () => { setPermissionSearch(event.target.value)} + onChange={(event) => + setPermissionSearch(event.target.value) + } placeholder="Search permissions by name or key" /> @@ -960,7 +1030,9 @@ const PositionTypesPage = () => { ); }} /> - Select all + + Select all + {loadingPermissionsCatalog ? ( @@ -976,20 +1048,29 @@ const PositionTypesPage = () => { > { setSelectedPermissionIds((current) => event.target.checked ? [...current, permission.id] - : current.filter((item) => item !== permission.id), + : current.filter( + (item) => item !== permission.id, + ), ); }} />
- {getLocaleLabel(permission.name, permission.key)} + {getLocaleLabel( + permission.name, + permission.key, + )} +
+
+ {permission.key}
-
{permission.key}
))} @@ -1030,30 +1111,44 @@ const PositionTypesPage = () => { Create position type - Add a new position type for the selected unit and optionally copy permissions from an existing one. + Add a new position type for the selected unit and optionally copy + permissions from an existing one. -
void handleCreatePositionType(event)}> + void handleCreatePositionType(event)} + >
@@ -1064,13 +1159,18 @@ const PositionTypesPage = () => { className={inputClassName} value={createForm.key} onChange={(event) => - setCreateForm((current) => ({ ...current, key: event.target.value })) + setCreateForm((current) => ({ + ...current, + key: event.target.value, + })) } /> ))} @@ -1163,7 +1271,8 @@ const PositionTypesPage = () => {
- The new position type will inherit permissions from the selected source. + The new position type will inherit permissions from the + selected source.
) : null} diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/iamConfig.ts b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/iamConfig.ts index 923efad0e..ec27b5c76 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/iamConfig.ts +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/user-management/iamConfig.ts @@ -2,7 +2,6 @@ import type { DesignConfig } from "@tria-plc/iamui"; import { FREIGHT_BRAND, - FREIGHT_BRAND_DARK, FREIGHT_BRAND_LIGHT, freightBrand, } from "@/theme/freight-brand"; @@ -48,7 +47,7 @@ export const iamConfig: DesignConfig = { }, layout: { userManagementView: "classic", - showTopBar: true, + showTopBar: true as any, sidebarWidth: "280px", sidebarCollapsedWidth: "80px", headerHeight: "80px", diff --git a/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx b/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx index 4370adac1..af69d4991 100644 --- a/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/documents/EditFileUploadSettingDialog.tsx @@ -17,15 +17,7 @@ import { Textarea } from "@/components/ui/textarea"; import { FileUploadEntity } from "@edr/types/freight"; import { useMutation } from "@tanstack/react-query"; import { api } from "@/services/api"; - -// import type { -// FileUploadEntity, -// FileUploadSetting, -// } from "@/types/fileUploadSettings"; -// import { -// useCreateFileUploadSetting, -// useUpdateFileUploadSetting, -// } from "@/hooks/useFileUploadSettings"; +import { FileUploadSetting } from "@/types/fileUploadSettings"; export interface EditFileUploadSettingDialogProps { mode?: "create" | "edit"; @@ -33,19 +25,6 @@ export interface EditFileUploadSettingDialogProps { children: ReactNode; } -const selectClass = - "flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-xs outline-none transition hover:border-slate-300 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20"; - -// const ENTITIES: FileUploadEntity[] = [ -// "customer", -// "booking", -// "consignment", -// "shipment", -// "invoice", -// "train", -// "other", -// ]; - export default function EditFileUploadSettingDialog({ mode = "create", setting, @@ -62,8 +41,12 @@ export default function EditFileUploadSettingDialog({ const [description, setDescription] = useState(setting?.description ?? ""); const [error, setError] = useState(null); - const createMutation = useMutation(api.fileUploadSettings.create.mutationOptions()); - const updateMutation = useMutation(api.fileUploadSettings.update.mutationOptions()); + const createMutation = useMutation( + api.fileUploadSettings.create.mutationOptions(), + ); + const updateMutation = useMutation( + api.fileUploadSettings.update.mutationOptions(), + ); const pending = createMutation.isPending || updateMutation.isPending; const reset = () => { diff --git a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx index f68ac32f4..d3e4387b0 100644 --- a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx @@ -3,6 +3,7 @@ import { Box, Card, Group, + Badge, Badge as MantineBadge, Select, Stack, @@ -28,7 +29,6 @@ import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { api } from "@/services/api"; import type { PaymentMethod, PaymentRow } from "@/services/payments.service"; import { - Badge, DataTable, DataTableFooter, usePagination, @@ -36,7 +36,12 @@ import { } from "@edr/ui-common"; const STATUS_TABS = [ - { key: "all", label: "All", statuses: undefined as string | undefined, icon: LayoutGrid }, + { + key: "all", + label: "All", + statuses: undefined as string | undefined, + icon: LayoutGrid, + }, { key: "success", label: "Success", statuses: "success", icon: CheckCircle2 }, { key: "processing", @@ -44,7 +49,12 @@ const STATUS_TABS = [ statuses: "processing,action-required", icon: Loader2, }, - { key: "failed", label: "Failed", statuses: "failed,canceled", icon: XCircle }, + { + key: "failed", + label: "Failed", + statuses: "failed,canceled", + icon: XCircle, + }, { key: "refunded", label: "Refunded", statuses: "refunded", icon: RotateCcw }, ] as const; @@ -81,13 +91,14 @@ function formatDate(iso: string | null): string { return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); + year: "numeric", + month: "short", + day: "numeric", + }); } -const tableHeader = "text-xs font-semibold uppercase tracking-wide text-muted-foreground"; +const tableHeader = + "text-xs font-semibold uppercase tracking-wide text-muted-foreground"; export default function PaymentsPage() { const { pagination, setPagination } = usePagination({ pageSize: 10 }); @@ -124,9 +135,9 @@ export default function PaymentsPage() { summary === undefined ? undefined : (summary.success ?? 0) + - (summary.processing ?? 0) + - (summary.failed ?? 0) + - (summary.refunded ?? 0), + (summary.processing ?? 0) + + (summary.failed ?? 0) + + (summary.refunded ?? 0), success: summary?.success, processing: summary?.processing, failed: summary?.failed, @@ -285,7 +296,10 @@ export default function PaymentsPage() { value={query} onChange={(e) => { setQuery(e.target.value); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + setPagination({ + pageIndex: 0, + pageSize: pagination.pageSize, + }); }} rightSection={ query && ( @@ -309,7 +323,10 @@ export default function PaymentsPage() { value={method} onChange={(value) => { setMethod(value); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + setPagination({ + pageIndex: 0, + pageSize: pagination.pageSize, + }); }} style={{ minWidth: 180 }} /> diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx index 783c0ffd4..ffb752a56 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx @@ -2,8 +2,18 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Navigate, useLocation, useParams } from "react-router-dom"; import { useAuth } from "@/auth/useAuth"; import { canAccessRuleEngineResource } from "@/lib/permissions"; -import type { ColumnDef } from "@tanstack/react-table"; -import { Box, Card, Button, Modal, Stack, Group, Text, List, Loader } from "@mantine/core"; +import type { ColumnDef } from "@edr/ui-common"; +import { + Box, + Card, + Button, + Modal, + Stack, + Group, + Text, + List, + Loader, +} from "@mantine/core"; import { Plus } from "lucide-react"; import { PageContainer, PageHeader } from "@/components/page"; @@ -60,16 +70,17 @@ const RuleEngineResourcePage = () => { const config = resourceSlug ? getRuleEngineResource(resourceSlug) : undefined; const defaultPath = category - ? `${RULE_ENGINE_CATEGORY_BASE_PATH[category]}/${ - category === "rules" ? DEFAULT_RULES_SLUG : DEFAULT_CONFIGURATION_SLUG - }` + ? `${RULE_ENGINE_CATEGORY_BASE_PATH[category]}/${category === "rules" ? DEFAULT_RULES_SLUG : DEFAULT_CONFIGURATION_SLUG + }` : `${RULE_ENGINE_CATEGORY_BASE_PATH.configuration}/${DEFAULT_CONFIGURATION_SLUG}`; const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [search, setSearch] = useState(""); const [formOpen, setFormOpen] = useState(false); const [editing, setEditing] = useState(null); - const [deleteTarget, setDeleteTarget] = useState(null); + const [deleteTarget, setDeleteTarget] = useState( + null, + ); const [chainOpen, setChainOpen] = useState(false); const [orderDialogOpen, setOrderDialogOpen] = useState(false); const { viewMode, setViewMode } = useRuleEngineViewMode( @@ -90,9 +101,9 @@ const RuleEngineResourcePage = () => { pageSize: pagination.pageSize, ...(config?.orderConfig ? { - sortBy: config.orderConfig.field, - sortOrder: "ASC" as const, - } + sortBy: config.orderConfig.field, + sortOrder: "ASC" as const, + } : {}), }), [ @@ -120,11 +131,12 @@ const RuleEngineResourcePage = () => { const { reorder, moveOrder } = useRuleEngineOrderMutations( config?.slug ?? DEFAULT_CONFIGURATION_SLUG, ); - const { data: orderListData, isLoading: orderListLoading } = useRuleEngineOrderList( - config?.slug ?? DEFAULT_CONFIGURATION_SLUG, - Boolean(orderDialogOpen && config?.orderConfig), - config?.orderConfig?.field, - ); + const { data: orderListData, isLoading: orderListLoading } = + useRuleEngineOrderList( + config?.slug ?? DEFAULT_CONFIGURATION_SLUG, + Boolean(orderDialogOpen && config?.orderConfig), + config?.orderConfig?.field, + ); const { submit, approve } = useRateWorkflow(); const { data: chainData, isLoading: chainLoading } = useApprovalChain( chainOpen && config?.slug === "approval-rules", @@ -141,10 +153,7 @@ const RuleEngineResourcePage = () => { const { data: cargoParentOptions, isLoading: cargoParentOptionsLoading } = useCargoTypeParentOptions(editingId, config?.slug === "cargo-types"); const { data: containerTypeOptions, isLoading: containerTypeOptionsLoading } = - useContainerTypeOptions( - config?.slug === "rates", - usesContainerTypeField, - ); + useContainerTypeOptions(config?.slug === "rates", usesContainerTypeField); const { data: liveRateOptions, isLoading: liveRateOptionsLoading } = useLiveRateOptions(usesLiveRateField); @@ -154,10 +163,9 @@ const RuleEngineResourcePage = () => { if (config.slug === "cargo-types" && field.name === "parentGroupId") { return { ...field, - options: - cargoParentOptions ?? [ - { label: "None", value: RULE_ENGINE_SELECT_NONE }, - ], + options: cargoParentOptions ?? [ + { label: "None", value: RULE_ENGINE_SELECT_NONE }, + ], }; } if (field.name === "containerTypeId") { @@ -183,14 +191,16 @@ const RuleEngineResourcePage = () => { const pageCount = meta?.totalPages ?? 1; const totalCount = meta?.total ?? rows.length; - const { data: createPositionList, isLoading: createPositionLoading } = useRuleEngineOrderList( - config?.slug ?? DEFAULT_CONFIGURATION_SLUG, - Boolean(formOpen && !editing && config?.orderConfig), - config?.orderConfig?.field, - ); + const { data: createPositionList, isLoading: createPositionLoading } = + useRuleEngineOrderList( + config?.slug ?? DEFAULT_CONFIGURATION_SLUG, + Boolean(formOpen && !editing && config?.orderConfig), + config?.orderConfig?.field, + ); const createPositionOptions = useMemo(() => { - if (!config?.orderConfig || !createPositionList?.data?.length) return undefined; + if (!config?.orderConfig || !createPositionList?.data?.length) + return undefined; return createPositionList.data .filter((row) => row.id) .map((row) => ({ @@ -199,7 +209,6 @@ const RuleEngineResourcePage = () => { })); }, [config?.orderConfig, config?.slug, createPositionList?.data]); - const handleApproveRate = useCallback( (record: RuleEngineRecord) => { approve.mutate(String(record.id)); @@ -259,7 +268,9 @@ const RuleEngineResourcePage = () => { }} onDelete={setDeleteTarget} onViewChain={ - config.slug === "approval-rules" ? () => setChainOpen(true) : undefined + config.slug === "approval-rules" + ? () => setChainOpen(true) + : undefined } onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined} onApproveRate={canManage ? handleApproveRate : undefined} @@ -270,7 +281,15 @@ const RuleEngineResourcePage = () => { }); return base; - }, [canManage, config, submit, handleApproveRate, handleMoveOrder, moveOrder.isPending, totalCount]); + }, [ + canManage, + config, + submit, + handleApproveRate, + handleMoveOrder, + moveOrder.isPending, + totalCount, + ]); const tableStatus = isLoading ? "loading" : isError ? "error" : "success"; @@ -350,15 +369,20 @@ const RuleEngineResourcePage = () => { onSearchChange={ config.supportsSearch ? (v) => { - setSearch(v); - setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); - } + setSearch(v); + setPagination({ + pageIndex: 0, + pageSize: pagination.pageSize, + }); + } : undefined } showSearch={Boolean(config.supportsSearch)} searchPlaceholder={config.searchPlaceholder} onManageOrder={ - canManage && config.orderConfig ? () => setOrderDialogOpen(true) : undefined + canManage && config.orderConfig + ? () => setOrderDialogOpen(true) + : undefined } viewMode={viewMode} onViewModeChange={setViewMode} @@ -373,10 +397,12 @@ const RuleEngineResourcePage = () => { error={ isError ? { - message: "Failed to load data", - description: - error instanceof Error ? error.message : "Unknown error", - } + message: "Failed to load data", + description: + error instanceof Error + ? error.message + : "Unknown error", + } : undefined } emptyMessage={`No ${itemLabel} found.`} @@ -422,7 +448,9 @@ const RuleEngineResourcePage = () => { onEdit={canManage ? openEdit : undefined} onDelete={canManage ? setDeleteTarget : undefined} onViewChain={ - config.slug === "approval-rules" ? () => setChainOpen(true) : undefined + config.slug === "approval-rules" + ? () => setChainOpen(true) + : undefined } onSubmitRate={canManage ? (id) => submit.mutate(id) : undefined} onApproveRate={canManage ? handleApproveRate : undefined} @@ -434,7 +462,11 @@ const RuleEngineResourcePage = () => { { > - This will soft-delete the selected {config.label.toLowerCase()} record. + This will soft-delete the selected {config.label.toLowerCase()}{" "} + record. - - {data.trainNumber ?? data.routeName ?? "Schedule"} - - - {data.status} - - - - }> - {data.scheduleDate - ? new Intl.DateTimeFormat("en-GB", { + + + + + + {data.trainNumber ?? data.routeName ?? "Schedule"} + + + {data.status} + + + + }> + {data.scheduleDate + ? new Intl.DateTimeFormat("en-GB", { weekday: "short", day: "2-digit", month: "short", @@ -653,317 +693,360 @@ export default function BatchScheduleDetailPage() { hour12: false, timeZone: "Africa/Addis_Ababa", }).format(new Date(data.scheduleDate)) + " EAT" - : "No date"} - - {data.locomotive ? ( - }> - Loco {data.locomotive.code} · {fmtTons(data.locomotive.maxPullWeightTons)} ·{" "} - {data.locomotive.maxTrainLengthMeters} m + : "No date"} - ) : null} - - - - - - - - - - - {!data.locomotive ? ( - }> - No locomotive assigned — wagon allocation cannot run. - - ) : null} - - - - {/* Booking pipeline */} - - - - - - - Booking pipeline - - - {totalBookings} booking{totalBookings === 1 ? "" : "s"} - - - - - - {data.allocationViolations.length ? ( - } - title="Allocation constraints" - > - - {data.allocationViolations.map((v) => ( - - {v} - - ))} - - - ) : null} - - {/* Batch windows */} - - - - - - - Batch windows (EAT) - - 3-hour windows for every day from when the booking window opened through the - departure date. Bookings appear under the date their contract was signed — open a - day to see its windows. - - - - - {dayGroups.length && selectedDay ? ( - <> - {/* Date stepper — page back/forward through each day in the range */} - - setSelectedDate(dayGroups[selectedIndex - 1]?.date ?? null)} - > - - - - - - - - {selectedDay.dateLabel} - - {selectedDay.date === todayEat ? ( - - Today - + {data.locomotive ? ( + }> + Loco {data.locomotive.code} ·{" "} + {fmtTons(data.locomotive.maxPullWeightTons)} ·{" "} + {data.locomotive.maxTrainLengthMeters} m + ) : null} - - {selectedDay.totalBookings - ? `${selectedDay.totalBookings} booking${selectedDay.totalBookings === 1 ? "" : "s"} · ${selectedDay.windows.length} windows` - : `${selectedDay.windows.length} windows · no bookings`} - - + - = dayGroups.length - 1} - onClick={() => setSelectedDate(dayGroups[selectedIndex + 1]?.date ?? null)} - > - - - - - - - Day {selectedIndex + 1} of {dayGroups.length} - - - {selectedDay.hasIssues ? ( - } - > - Issues - - ) : null} - + + + + - - {selectedDay.windows.map((window) => ( - - ))} - - - ) : ( - - No batch windows for this schedule. - - )} + {!data.locomotive ? ( + }> + No locomotive assigned — wagon allocation cannot run. + + ) : null} - {data.pendingContract.bookings.length ? ( - - - - - - + + {/* Booking pipeline */} + + + + + + + Booking pipeline + + + {totalBookings} booking{totalBookings === 1 ? "" : "s"} + + + + + + {data.allocationViolations.length ? ( + } + title="Allocation constraints" + > + + {data.allocationViolations.map((v) => ( + + {v} + + ))} + + + ) : null} + + {/* Batch windows */} + + + + + + + Batch windows (EAT) + + 3-hour windows for every day from when the booking window + opened through the departure date. Bookings appear under the + date their contract was signed — open a day to see its + windows. + + + + + {dayGroups.length && selectedDay ? ( + <> + {/* Date stepper — page back/forward through each day in the range */} + + + setSelectedDate( + dayGroups[selectedIndex - 1]?.date ?? null, + ) + } + > + + + + - - - - - Pending contract + + + + {selectedDay.dateLabel} + + {selectedDay.date === todayEat ? ( + + Today + + ) : null} + + + {selectedDay.totalBookings + ? `${selectedDay.totalBookings} booking${selectedDay.totalBookings === 1 ? "" : "s"} · ${selectedDay.windows.length} windows` + : `${selectedDay.windows.length} windows · no bookings`} - - Contract not signed yet — not in any window - - - - - {data.pendingContract.bookings.length} booking - {data.pendingContract.bookings.length === 1 ? "" : "s"} - - - - - - - - - ) : null} - + - {/* Train composition diagram */} - {hasAssignedWagons && scheduleDetailQuery.data ? ( - - - - ) : null} + = dayGroups.length - 1} + onClick={() => + setSelectedDate( + dayGroups[selectedIndex + 1]?.date ?? null, + ) + } + > + + + + + + + Day {selectedIndex + 1} of {dayGroups.length} + + + {selectedDay.hasIssues ? ( + } + > + Issues + + ) : null} + + + + + + {selectedDay.windows.map((window) => ( + + ))} + + + ) : ( + + No batch windows for this schedule. + + )} + + {data.pendingContract.bookings.length ? ( + + + + + + + + + + + Pending contract + + + Contract not signed yet — not in any window + + + + + {data.pendingContract.bookings.length} booking + {data.pendingContract.bookings.length === 1 + ? "" + : "s"} + + + + + + + + + ) : null} + + + {/* Train composition diagram */} + {hasAssignedWagons && scheduleDetailQuery.data ? ( + + + + ) : null} diff --git a/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts b/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts index 9f092f46d..7cab23f64 100644 --- a/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts @@ -67,7 +67,11 @@ const byIdPath = (resource: RuleEngineResourceSlug, id: string): string => { } }; -const defaultMeta = (dataLength: number, page = 1, pageSize = 10): RuleEngineListMeta => ({ +const defaultMeta = ( + dataLength: number, + page = 1, + pageSize = 10, +): RuleEngineListMeta => ({ total: dataLength, page, pageSize, @@ -79,7 +83,7 @@ const isPaginatedListResult = ( ): value is RuleEngineListResult => Boolean(value) && typeof value === "object" && - "data" in value && + "data" in (value ?? {}) && Array.isArray((value as RuleEngineListResult).data); const normalizeList = ( @@ -104,7 +108,10 @@ const normalizeList = ( } if (Array.isArray(body)) { - return { data: body as T[], meta: defaultMeta(body.length, page, pageSize) }; + return { + data: body as T[], + meta: defaultMeta(body.length, page, pageSize), + }; } return { data: [], meta: defaultMeta(0, page, pageSize) }; @@ -161,7 +168,10 @@ export const ruleEngineService = { return normalizeEntity(response.data); }, - remove: async (resource: RuleEngineResourceSlug, id: string): Promise => { + remove: async ( + resource: RuleEngineResourceSlug, + id: string, + ): Promise => { await client.delete(byIdPath(resource, id)); }, @@ -181,24 +191,36 @@ export const ruleEngineService = { }, submitRate: async (id: string): Promise => { - const response = await client.post(URL_CONSTANTS.RULE_ENGINE.RATE_SUBMIT(id)); + const response = await client.post( + URL_CONSTANTS.RULE_ENGINE.RATE_SUBMIT(id), + ); return normalizeEntity(response.data); }, approveRate: async (id: string): Promise => { - const response = await client.post(URL_CONSTANTS.RULE_ENGINE.RATE_APPROVE(id)); + const response = await client.post( + URL_CONSTANTS.RULE_ENGINE.RATE_APPROVE(id), + ); return normalizeEntity(response.data); }, getApprovalChain: async ( requiresDirectorApproval = true, ): Promise => { - const response = await client.get(URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULES_CHAIN, { - params: { requiresDirectorApproval }, - }); + const response = await client.get( + URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULES_CHAIN, + { + params: { requiresDirectorApproval }, + }, + ); const body = unwrap(response.data) as unknown; if (Array.isArray(body)) return body as RuleEngineRecord[]; - if (body && typeof body === "object" && "data" in body && Array.isArray((body as { data: unknown }).data)) { + if ( + body && + typeof body === "object" && + "data" in body && + Array.isArray((body as { data: unknown }).data) + ) { return (body as { data: RuleEngineRecord[] }).data; } return []; diff --git a/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts b/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts index 63929fd2e..2c8669137 100644 --- a/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/trainScheduling.service.ts @@ -1,13 +1,12 @@ -import { api as client } from '../auth/http'; -import { unwrap } from '@/utils/endpoint'; -import { URL_CONSTANTS } from '@/constants/URLS'; +import { api as client } from "../auth/http"; +import { unwrap } from "@/utils/endpoint"; +import { URL_CONSTANTS } from "@/constants/URLS"; import type { BatchBoardSchedule, BatchBoardScheduleDetail, BookableSchedule, AssignBookingsPayload, CompositionRemovalEntry, - CompositionUnassignedBooking, UnassignedBookingsResponse, CreateTrainSchedulePayload, EligibleContainerBookingsResponse, @@ -24,7 +23,7 @@ import type { TrainTrackResponse, WagonAllocationAttemptResult, YardOption, -} from '@/types/trainScheduling'; +} from "@/types/trainScheduling"; interface BookingReferenceDataResponse { yard?: Array; @@ -58,7 +57,9 @@ export const trainSchedulingService = { ): Promise => { const useUnified = !freightType || freightType === "MIXED"; const response = await client.post( - useUnified ? URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW : pathsFor(freightType).PREVIEW, + useUnified + ? URL_CONSTANTS.TRAIN_SCHEDULING.PREVIEW + : pathsFor(freightType).PREVIEW, payload, ); return unwrap(response.data); @@ -91,7 +92,9 @@ export const trainSchedulingService = { return unwrap(response.data); }, - getBatchBoardDetail: async (scheduleId: string): Promise => { + getBatchBoardDetail: async ( + scheduleId: string, + ): Promise => { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.BATCH_BOARD_DETAIL(scheduleId), ); @@ -132,7 +135,9 @@ export const trainSchedulingService = { return unwrap(response.data); }, - runAllocation: async (scheduleId: string): Promise => { + runAllocation: async ( + scheduleId: string, + ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.RUN_ALLOCATION(scheduleId), {}, @@ -152,20 +157,29 @@ export const trainSchedulingService = { }, markBookingPaid: async (bookingId: string): Promise => { - await client.post(URL_CONSTANTS.TRAIN_SCHEDULING.MARK_BOOKING_PAID(bookingId), {}); + await client.post( + URL_CONSTANTS.TRAIN_SCHEDULING.MARK_BOOKING_PAID(bookingId), + {}, + ); }, expireBooking: async (bookingId: string): Promise => { - await client.post(URL_CONSTANTS.TRAIN_SCHEDULING.EXPIRE_BOOKING(bookingId), {}); + await client.post( + URL_CONSTANTS.TRAIN_SCHEDULING.EXPIRE_BOOKING(bookingId), + {}, + ); }, moveBookingSchedule: async ( bookingId: string, trainScheduleId: string, ): Promise => { - await client.post(URL_CONSTANTS.TRAIN_SCHEDULING.MOVE_BOOKING_SCHEDULE(bookingId), { - trainScheduleId, - }); + await client.post( + URL_CONSTANTS.TRAIN_SCHEDULING.MOVE_BOOKING_SCHEDULE(bookingId), + { + trainScheduleId, + }, + ); }, getScheduleById: async ( @@ -173,7 +187,9 @@ export const trainSchedulingService = { freightType?: FreightType, ): Promise => { const response = await client.get( - pathsFor(freightType === "MIXED" ? undefined : freightType).SCHEDULE_BY_ID(id), + pathsFor( + freightType === "MIXED" ? undefined : freightType, + ).SCHEDULE_BY_ID(id), ); return unwrap(response.data); }, @@ -225,7 +241,9 @@ export const trainSchedulingService = { return unwrap(response.data); }, - finalizeSchedule: async (scheduleId: string): Promise => { + finalizeSchedule: async ( + scheduleId: string, + ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.FINALIZE(scheduleId), {}, @@ -233,7 +251,9 @@ export const trainSchedulingService = { return unwrap(response.data); }, - dispatchSchedule: async (scheduleId: string): Promise => { + dispatchSchedule: async ( + scheduleId: string, + ): Promise => { const response = await client.post( URL_CONSTANTS.TRAIN_SCHEDULING.DISPATCH(scheduleId), {}, @@ -272,13 +292,17 @@ export const trainSchedulingService = { freightType: FreightType = "CONTAINER", ): Promise => { const response = await client.post( - pathsFor(freightType === "MIXED" ? undefined : freightType).CANCEL_SCHEDULE(id), + pathsFor( + freightType === "MIXED" ? undefined : freightType, + ).CANCEL_SCHEDULE(id), {}, ); return unwrap(response.data); }, - getAvailableLocomotives: async (routeId?: string): Promise => { + getAvailableLocomotives: async ( + routeId?: string, + ): Promise => { if (routeId) { const response = await client.get( URL_CONSTANTS.TRAIN_SCHEDULING.AVAILABLE_LOCOMOTIVES, @@ -286,9 +310,12 @@ export const trainSchedulingService = { ); return unwrap(response.data); } - const response = await client.get(URL_CONSTANTS.LOCOMOTIVES.BASE, { - params: { status: 'AVAILABLE' }, - }); + const response = await client.get( + URL_CONSTANTS.LOCOMOTIVES.BASE, + { + params: { status: "AVAILABLE" }, + }, + ); return unwrap(response.data); }, @@ -380,7 +407,10 @@ export const trainSchedulingService = { })); }, - removeWagonSlot: async (scheduleId: string, wagonId: string): Promise => { + removeWagonSlot: async ( + scheduleId: string, + wagonId: string, + ): Promise => { const response = await client.delete( URL_CONSTANTS.TRAIN_SCHEDULING.REMOVE_WAGON_SLOT(scheduleId, wagonId), ); @@ -392,7 +422,10 @@ export const trainSchedulingService = { itemId: string, payload: { containerNumber: string | null }, ): Promise<{ id: string; containerNumber: string | null }> => { - const response = await client.patch<{ id: string; containerNumber: string | null }>( + const response = await client.patch<{ + id: string; + containerNumber: string | null; + }>( URL_CONSTANTS.TRAIN_SCHEDULING.UPDATE_CONTAINER_ITEM(scheduleId, itemId), payload, ); diff --git a/apps/edr-freight-web/backoffice/src/types/@tria-plc__iamui.d.ts b/apps/edr-freight-web/backoffice/src/types/@tria-plc__iamui.d.ts new file mode 100644 index 000000000..d0bdfe618 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/types/@tria-plc__iamui.d.ts @@ -0,0 +1,55 @@ +declare module "@tria-plc/iamui" { + import type { ComponentType } from "react"; + + export interface DesignConfig { + brand: { appName: string; logoUrl: string }; + colors: Record; + typography: { + fontFamily: string; + headingFontFamily: string; + baseFontSize: string; + fontWeight: string; + }; + shape: { radius: string }; + shadows: Record; + components: { + buttonDefaultVariant?: string; + inputDefaultSize?: string; + inputRadius?: string; + modalRadius?: string; + tableHighlightOnHover?: boolean; + }; + layout: Record>; + appearance: { + colorScheme: string; + slots: Record }>; + customCss: string; + }; + } + + export interface UserManagementRuntimeOptions { + basename: string; + apiBaseUrl: string; + apiUrl: string; + recordApiUrl: string; + chronicleUrl: string; + auditApiUrl: string; + } + + export interface UserManagementSessionSeed { + token: string; + refreshToken?: string; + rememberMe: boolean; + } + + export interface UserManagementAppProps { + config: DesignConfig; + runtime: UserManagementRuntimeOptions; + session: { + initialSession: UserManagementSessionSeed | null; + enableEmbeddedAuthBridge: boolean; + }; + } + + export const UserManagementApp: ComponentType; +} diff --git a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts index 3195281b2..805e48132 100644 --- a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts +++ b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts @@ -146,14 +146,12 @@ export interface TrainScheduleListItem { origin: string | null; destination: string | null; freightType?: FreightType | null; - locomotive: - | { - id: string; - code: string; - name?: string | null; - currentYardId?: string | null; - } - | null; + locomotive: { + id: string; + code: string; + name?: string | null; + currentYardId?: string | null; + } | null; wagonCount: number; totalWeightTons: number; totalLengthMeters: number; @@ -316,7 +314,6 @@ export interface TrainScheduleWagonAllocation { export interface TrainScheduleDetail { id: string; status: TrainScheduleStatus | string; - warnings?: string[]; deferredBookings?: DeferredBookingRow[]; freightType?: FreightType | null; trainNumber?: string | null; diff --git a/apps/edr-freight-web/backoffice/tsconfig.app.json b/apps/edr-freight-web/backoffice/tsconfig.app.json index 9c7064567..079051c3d 100644 --- a/apps/edr-freight-web/backoffice/tsconfig.app.json +++ b/apps/edr-freight-web/backoffice/tsconfig.app.json @@ -5,7 +5,8 @@ "useDefineForClassFields": true, "skipLibCheck": true, "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@tria-plc/iamui": ["./src/types/@tria-plc__iamui.d.ts"] } }, "include": ["src"] From cbbad02b2996e8869fcc5f22b7582de9b8b97a53 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 23 Jun 2026 13:14:06 +0000 Subject: [PATCH 2/3] fix: type error and other fixes --- .../modules/companies/companies.service.ts | 1 + .../train-scheduling.service.ts | 1 + .../bookings/detail/booking-detail.styles.ts | 2 + .../compositionEditor/BookingDetailModal.tsx | 2 +- .../InteractiveTrainConsist.tsx | 2 +- .../compositionEditor/RemoveBookingModal.tsx | 7 +- .../compositionEditor/TrainConsistView.tsx | 6 +- .../compositionEditor/WagonCard.tsx | 2 +- .../warehouses/WarehouseInventoryTable.tsx | 17 ++-- .../src/pages/bookings/NewBookingPage.tsx | 2 +- .../src/pages/dashboard/OverviewPage.tsx | 2 +- .../src/pages/fleet/FleetCrudPages.tsx | 62 +++++++------- .../src/pages/fleet/FleetResourcePage.tsx | 83 ++++--------------- .../ruleEngine/RuleEngineResourcePage.tsx | 68 ++++++++------- .../src/services/customers.service.ts | 1 + .../backoffice/src/types/trainScheduling.ts | 1 + .../utils/groupBookingsForOperationsQueue.ts | 2 +- .../portal/src/services/auth.service.ts | 34 ++++---- 18 files changed, 116 insertions(+), 179 deletions(-) diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index a4466efb2..12fcb6238 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -282,6 +282,7 @@ export class CompaniesService { async findCompanyById(id: string): Promise { const company = await this.companiesRepo.findById(id); if (!company) throw new NotFoundException(`Company ${id} not found`); + company.companyProfiles = await this.companyProfilesRepo.findByCompanyId(id); return company; } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts index 3928376ce..239640624 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts @@ -2152,6 +2152,7 @@ export class TrainSchedulingService { weightTons: roundTons(Number(sb.booking?.cargoTotalWeightVgm ?? 0)), status: sb.booking?.status ?? null, schedulingStatus: sb.booking?.schedulingStatus ?? null, + freightType: sb.booking?.freightType ?? null, })) ?? [], }; } diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts b/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts index ccee1e8cb..95fe64016 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts @@ -138,6 +138,8 @@ export interface BookingDetailView { priorityScore: number; cargoTotalWeightVgm: number; pnrCode?: string | null; + consolidationPartnerId?: string | null; + consolidationPartner?: (BookingNamedRefView & { reference?: string }) | null; /** End of the pay window once the booking is SELECTED_FOR_BATCH. */ paymentDeadline?: string | null; createdAt: string; diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/BookingDetailModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/BookingDetailModal.tsx index 2243d1650..00df4700c 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/BookingDetailModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/BookingDetailModal.tsx @@ -11,7 +11,7 @@ import { import type { TrainScheduleDetail } from "@/types/trainScheduling"; import { freightBrand } from "@/theme/freight-brand"; -type Wagon = TrainScheduleDetail["trainSet"]["wagons"][number]; +type Wagon = NonNullable["wagons"][number]; export interface BookingDetailData { bookingId: string; diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/InteractiveTrainConsist.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/InteractiveTrainConsist.tsx index 297ecc48b..ee69fe145 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/InteractiveTrainConsist.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/InteractiveTrainConsist.tsx @@ -11,7 +11,7 @@ import { import type { TrainScheduleDetail } from "@/types/trainScheduling"; import { freightBrand } from "@/theme/freight-brand"; -type Wagon = TrainScheduleDetail["trainSet"]["wagons"][number]; +type Wagon = NonNullable["wagons"][number]; type Locomotive = NonNullable["locomotive"]; interface InteractiveTrainConsistProps { diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemoveBookingModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemoveBookingModal.tsx index 151a796d8..4d2b1369c 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemoveBookingModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/RemoveBookingModal.tsx @@ -1,7 +1,7 @@ import { Button, Group, Modal, Stack, Text, Badge } from "@mantine/core"; import type { TrainScheduleDetail } from "@/types/trainScheduling"; -type WagonWithAllocation = TrainScheduleDetail["trainSet"]["wagons"][number]; +type WagonWithAllocation = NonNullable["wagons"][number]; interface RemoveBookingModalProps { opened: boolean; @@ -21,7 +21,6 @@ export const RemoveBookingModal = ({ if (!wagon || !wagon.allocations?.[0]) return null; const allocation = wagon.allocations[0]; - const booking = allocation.booking; return ( @@ -32,12 +31,12 @@ export const RemoveBookingModal = ({ - Reference: {booking?.reference || "N/A"} + Reference: {allocation.bookingReference || "N/A"} Freight Type:{" "} - {booking?.freightType || "N/A"} + {allocation.loadType || "N/A"} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx index 2ebfbb8ff..6ece98ef2 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/TrainConsistView.tsx @@ -10,7 +10,7 @@ import { useMutation } from "@tanstack/react-query"; import { api } from "@/services/api"; import { freightBrand } from "@/theme/freight-brand"; -type Wagon = TrainScheduleDetail["trainSet"]["wagons"][number]; +type Wagon = NonNullable["wagons"][number]; interface TrainConsistViewProps { scheduleDetail: TrainScheduleDetail; @@ -103,9 +103,9 @@ export const TrainConsistView = ({ diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/WagonCard.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/WagonCard.tsx index a3fb971e4..663ab2e6f 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/WagonCard.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/compositionEditor/WagonCard.tsx @@ -12,7 +12,7 @@ import type { TrainScheduleDetail } from "@/types/trainScheduling"; import { ContainerNumberInput } from "./ContainerNumberInput"; import { freightBrand } from "@/theme/freight-brand"; -type Wagon = TrainScheduleDetail["trainSet"]["wagons"][number]; +type Wagon = NonNullable["wagons"][number]; interface WagonCardProps { wagon: Wagon; diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx index ec6f0f7d0..5598870c4 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/WarehouseInventoryTable.tsx @@ -1,11 +1,12 @@ -import { useMemo } from "react"; +import { DataTable, type ColumnDef } from "@edr/ui-common"; import { ActionIcon, Badge, Button, Group, Text, Tooltip } from "@mantine/core"; import { ArrowRightLeft, ClipboardList, Coins, History } from "lucide-react"; -import { DataTable, type ColumnDef } from "@edr/ui-common"; +import { useMemo } from "react"; -import type { - InventoryAction, - WarehouseInventoryItem, +import { + INVENTORY_NEXT_ACTION, + type InventoryAction, + type WarehouseInventoryItem, } from "@/types/warehouse"; import { InventoryStatusBadge } from "./badges"; import { formatDate, formatNumber, humanizeEnum } from "./options"; @@ -54,12 +55,6 @@ export function WarehouseInventoryTable({ onHistory, onInspect, onFeePreview, - onLastMile, - selectedIds, - onToggleSelect, - onToggleSelectAll, - allSelected, - someSelected, }: WarehouseInventoryTableProps) { const columns = useMemo[]>( () => [ diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx index b3c9db35d..735f95c07 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/NewBookingPage.tsx @@ -662,7 +662,7 @@ export default function NewBookingPage() { placeholder="Select bulk cargo type" data={cargoData} value={cargoTypeId} - onChange={setCargoTypeId} + onChange={(value) => setCargoTypeId(value as string | null)} searchable disabled={isLoading} /> diff --git a/apps/edr-freight-web/backoffice/src/pages/dashboard/OverviewPage.tsx b/apps/edr-freight-web/backoffice/src/pages/dashboard/OverviewPage.tsx index 8bcec226c..427787146 100644 --- a/apps/edr-freight-web/backoffice/src/pages/dashboard/OverviewPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/dashboard/OverviewPage.tsx @@ -93,7 +93,7 @@ const OverviewPage = () => { const getTabBadge = (tab: (typeof TAB_ITEMS)[number]) => { if (!summary?.kpis) return 0; - const group = summary.kpis[tab.kpiKey] as Record; + const group = summary.kpis[tab.kpiKey] as unknown as Record; return group[tab.metricKey] ?? 0; }; diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx index cd846ada7..c8cbaa453 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetCrudPages.tsx @@ -1,49 +1,49 @@ -import { FormEvent, ReactNode, useMemo, useState } from 'react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { Edit, Eye, Plus, Search, Trash2 } from 'lucide-react'; +import { FormEvent, ReactNode, useMemo, useState } from 'react'; import { api } from '@/services/api'; import { - ActionIcon, - Badge as MantineBadge, - Box, - Button as MantineButton, - Group, - Modal, - NumberInput, - Pagination, - Paper, - ScrollArea, - Select as MantineSelect, - SimpleGrid, - Stack, - Table as MantineTable, - Text, - TextInput, - Title, + ActionIcon, + Box, + Group, + Badge as MantineBadge, + Button as MantineButton, + Select as MantineSelect, + Table as MantineTable, + Modal, + NumberInput, + Pagination, + Paper, + ScrollArea, + SimpleGrid, + Stack, + Text, + TextInput, + Title, } from '@mantine/core'; +import { DeliverCargoDialog } from '@/components/cargoes/DeliverCargoDialog'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { useToast } from '@/hooks/use-toast'; import type { Cargo } from '@/services/cargoService'; -import { DeliverCargoDialog } from '@/components/cargoes/DeliverCargoDialog'; import type { Container } from '@/services/containerService'; import type { Locomotive } from '@/services/locomotives.service'; import type { Train } from '@/services/trains.service'; -import type { Wagon } from '@/services/wagon.service'; import type { WagonType } from '@/services/wagon-types.service'; +import type { Wagon } from '@/services/wagon.service'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@edr/ui-common'; type FormValue = string | number | boolean | string[]; @@ -331,7 +331,7 @@ function FleetCrudPage({ {columns.map((column) => ( - {column.render ? column.render(item) : String((item as Record)[column.key] ?? '-')} + {column.render ? column.render(item) : String((item as Record)[String(column.key)] ?? '-')} ))} @@ -410,7 +410,7 @@ function FleetCrudPage({ ...current, [field.key]: selectedValue, ...(field.onValueChange?.(selectedValue, current) ?? {}), - })) + }) as Record) } > @@ -480,12 +480,6 @@ function FleetCrudPage({ const statusBadge = (status?: string) => {status ?? '-'}; -const activeBadge = (isActive?: boolean) => ( - - {isActive === false ? 'Inactive' : 'Active'} - -); - const optionLabel = (options: { value: string; label: string }[], value?: string | null) => options.find((option) => option.value === value)?.label ?? value ?? '-'; diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx index 6db31e931..5be9f1e56 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx @@ -1,19 +1,10 @@ import type { ColumnDef } from "@edr/ui-common"; -import { Box, Button, Card, Group, Modal, Select, Stack, Text } from "@mantine/core"; +import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, Title } from "@mantine/core"; import { useMutation, useQuery } from "@tanstack/react-query"; import { api } from "@/services/api"; -import { - Archive, - Circle, - CircleCheck, - CircleSlash, - Layers, - Link2, - Plus, - Wrench, - type LucideIcon, -} from "lucide-react"; +import Breadcrumbs from "@/components/ui/Breadcrumbs"; +import { Plus } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { Navigate, useLocation } from "react-router-dom"; @@ -23,37 +14,20 @@ import FleetRecordActions from "@/components/fleet/FleetRecordActions"; import FleetToolbar from "@/components/fleet/FleetToolbar"; import { formatFleetCell, registerFleetOptionLabels } from "@/components/fleet/fleetFormat"; import { useFleetViewMode } from "@/components/fleet/useFleetViewMode"; -import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { useToast } from "@/hooks/use-toast"; import { - FLEET_SELECT_NONE, - getFleetResource, - getFleetSlugFromPath, - type FleetFormFieldDef, - type FleetResourceSlug, + FLEET_SELECT_NONE, + getFleetResource, + getFleetSlugFromPath, + type FleetFormFieldDef, + type FleetResourceSlug, } from "@/pages/fleet/config/resources"; import type { FleetListFilters, FleetRecord } from "@/services/fleet/fleet.service"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; const DEFAULT_SLUG: FleetResourceSlug = "locomotives"; -const FLEET_STATUS_META: Record< - string, - { label: string; icon: LucideIcon; color: string } -> = { - AVAILABLE: { label: "Available", icon: CircleCheck, color: "edr-green" }, - ASSIGNED: { label: "Assigned", icon: Link2, color: "blue" }, - MAINTENANCE: { label: "Maintenance", icon: Wrench, color: "yellow" }, - OUT_OF_SERVICE: { label: "Out of service", icon: CircleSlash, color: "red" }, - RETIRED: { label: "Retired", icon: Archive, color: "gray" }, -}; - -const humanizeStatus = (status: string) => { - const text = status.replace(/_/g, " ").toLowerCase(); - return text.charAt(0).toUpperCase() + text.slice(1); -}; - const FleetResourcePage = () => { const location = useLocation(); const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG; @@ -113,6 +87,9 @@ const FleetResourcePage = () => { const { data: yards = [], isLoading: yardsLoading } = useQuery( api.routes.yards.queryOptions(), ); + const { data: drivers = [] } = useQuery( + api.fleet.list.queryOptions({ input: { slug: "drivers" } }), + ); useEffect(() => { setPagination((prev) => ({ pageIndex: 0, pageSize: prev.pageSize })); @@ -301,39 +278,6 @@ const FleetResourcePage = () => { const tableStatus = isLoading ? "loading" : isError ? "error" : "success"; - const kpiItems = useMemo(() => { - const items = [ - { - label: `Total ${config?.label.toLowerCase() ?? ""}`, - value: allRows.length, - icon: Layers, - color: "edr-green", - }, - ]; - if (hasStatusColumn) { - const counts = new Map(); - for (const row of allRows) { - const status = String( - (row as unknown as Record).status ?? "", - ); - if (status) counts.set(status, (counts.get(status) ?? 0) + 1); - } - const top = [...counts.entries()] - .sort((a, b) => b[1] - a[1]) - .slice(0, 4); - for (const [status, count] of top) { - const meta = FLEET_STATUS_META[status]; - items.push({ - label: meta?.label ?? humanizeStatus(status), - value: count, - icon: meta?.icon ?? Circle, - color: meta?.color ?? "gray", - }); - } - } - return items.slice(0, 5); - }, [allRows, hasStatusColumn, config?.label]); - if (!config) { return ; } @@ -376,7 +320,7 @@ const FleetResourcePage = () => { const handleAssignDriver = async () => { if (!assigningDriver || !("id" in assigningDriver) || !selectedDriver) return; try { - const selectedDriverRecord = (drivers as Array>).find( + const selectedDriverRecord = (drivers as unknown as Array>).find( (d) => String(d.id) === selectedDriver ); if (!selectedDriverRecord) return; @@ -384,6 +328,7 @@ const FleetResourcePage = () => { const driverName = `${selectedDriverRecord.firstName} ${selectedDriverRecord.lastName}`; await update.mutateAsync({ + slug, id: String(assigningDriver.id), data: { assignedDriverId: selectedDriver, @@ -608,7 +553,7 @@ const FleetResourcePage = () => { clearable value={selectedDriver} onChange={(value) => setSelectedDriver(value || "")} - data={(drivers as Array>).map((driver) => ({ + data={(drivers as unknown as Array>).map((driver) => ({ value: String(driver.id || ""), label: `${driver.firstName} ${driver.lastName} (${driver.licenseNumber})`, }))} diff --git a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx index ffb752a56..2009204a7 100644 --- a/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/ruleEngine/RuleEngineResourcePage.tsx @@ -1,58 +1,56 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { Navigate, useLocation, useParams } from "react-router-dom"; import { useAuth } from "@/auth/useAuth"; import { canAccessRuleEngineResource } from "@/lib/permissions"; import type { ColumnDef } from "@edr/ui-common"; import { - Box, - Card, - Button, - Modal, - Stack, - Group, - Text, - List, - Loader, + Box, + Button, + Card, + Group, + List, + Loader, + Modal, + Stack, + Text, } from "@mantine/core"; import { Plus } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Navigate, useLocation, useParams } from "react-router-dom"; import { PageContainer, PageHeader } from "@/components/page"; +import ManageRuleEngineOrderDialog from "@/components/ruleEngine/ManageRuleEngineOrderDialog"; import RuleEngineCardGrid from "@/components/ruleEngine/RuleEngineCardGrid"; import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog"; -import ManageRuleEngineOrderDialog from "@/components/ruleEngine/ManageRuleEngineOrderDialog"; import RuleEngineOrderControls from "@/components/ruleEngine/RuleEngineOrderControls"; import RuleEngineRecordActions from "@/components/ruleEngine/RuleEngineRecordActions"; -import { getOrderItemLabel } from "@/components/ruleEngine/ruleEngineOrder.utils"; import RuleEngineToolbar from "@/components/ruleEngine/RuleEngineToolbar"; import { formatCell } from "@/components/ruleEngine/ruleEngineFormat"; +import { getOrderItemLabel } from "@/components/ruleEngine/ruleEngineOrder.utils"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { useRuleEngineViewMode } from "@/components/ruleEngine/useRuleEngineViewMode"; import { - DEFAULT_CONFIGURATION_SLUG, - DEFAULT_RULES_SLUG, - RULE_ENGINE_CATEGORY_BASE_PATH, - RULE_ENGINE_SELECT_NONE, - getRuleEngineResource, - type RuleEngineNavCategory, -} from "@/pages/ruleEngine/config/resources"; -import { - useApprovalChain, - useCargoTypeParentOptions, - useContainerTypeOptions, - useLiveRateOptions, - useRateWorkflow, - useRuleEngineList, - useRuleEngineMutations, - useRuleEngineOrderList, - useRuleEngineOrderMutations, + useApprovalChain, + useCargoTypeParentOptions, + useContainerTypeOptions, + useLiveRateOptions, + useRateWorkflow, + useRuleEngineList, + useRuleEngineMutations, + useRuleEngineOrderList, + useRuleEngineOrderMutations, } from "@/hooks/rule-engine/useRuleEngine"; +import { + DEFAULT_CONFIGURATION_SLUG, + DEFAULT_RULES_SLUG, + RULE_ENGINE_CATEGORY_BASE_PATH, + RULE_ENGINE_SELECT_NONE, + getRuleEngineResource, + type RuleEngineNavCategory, +} from "@/pages/ruleEngine/config/resources"; import type { RuleEngineRecord } from "@/types/rule-engine"; import { - DataTable, - DataTableFooter, - getCoreRowModel, - usePagination, - useReactTable, + DataTable, + DataTableFooter, + usePagination, } from "@edr/ui-common"; const pathCategory = (pathname: string): RuleEngineNavCategory | undefined => { diff --git a/apps/edr-freight-web/backoffice/src/services/customers.service.ts b/apps/edr-freight-web/backoffice/src/services/customers.service.ts index d375ad7a7..4e9c9d80e 100644 --- a/apps/edr-freight-web/backoffice/src/services/customers.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/customers.service.ts @@ -24,6 +24,7 @@ function mapCompany(dto: Record): Company { const attrs = (dto.attributes as Record | null) ?? {}; return { ...(dto as unknown as Company), + companyProfiles: (dto.companyProfiles as Company["companyProfiles"]) ?? [], contactPersonName: (attrs.contactPersonName as string | null) ?? null, contactPersonPhone: (attrs.contactPersonPhone as string | null) ?? null, generalManagerName: (attrs.generalManagerName as string | null) ?? null, diff --git a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts index 805e48132..db23e0de5 100644 --- a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts +++ b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts @@ -375,6 +375,7 @@ export interface TrainScheduleDetail { weightTons: number; status: string | null; schedulingStatus?: SchedulingStatus | null; + freightType?: FreightType | string | null; }>; warnings?: string[]; } diff --git a/apps/edr-freight-web/backoffice/src/utils/groupBookingsForOperationsQueue.ts b/apps/edr-freight-web/backoffice/src/utils/groupBookingsForOperationsQueue.ts index 2cdd10088..e13416c57 100644 --- a/apps/edr-freight-web/backoffice/src/utils/groupBookingsForOperationsQueue.ts +++ b/apps/edr-freight-web/backoffice/src/utils/groupBookingsForOperationsQueue.ts @@ -7,7 +7,7 @@ import { export interface OperationsQueueGroups { government: BookingListRow[]; - commercial: ThreeHourBookingBucket[]; + commercial: ThreeHourBookingBucket[]; } export function groupBookingsForOperationsQueue( diff --git a/apps/edr-freight-web/portal/src/services/auth.service.ts b/apps/edr-freight-web/portal/src/services/auth.service.ts index 856d417cc..e1de1889a 100644 --- a/apps/edr-freight-web/portal/src/services/auth.service.ts +++ b/apps/edr-freight-web/portal/src/services/auth.service.ts @@ -1,40 +1,40 @@ import { URL_CONSTANTS } from "@/constants/URLS"; +import type { + AuthUser, + GenerateVerificationCodePayload, + LoginPayload, + LoginResponse, + OtpPayload, + OtpResponse, + SetPasswordPayload, + SignupPayload, + SignupResponse, +} from "@/types/auth"; import { client } from "@/utils/api"; import { ApiResponse } from "@edr/types"; -import type { - AuthUser, - GenerateVerificationCodePayload, - LoginPayload, - LoginResponse, - OtpPayload, - OtpResponse, - SetPasswordPayload, - SignupPayload, - SignupResponse, -} from "@/types/auth"; export const authService = { login: async (body: LoginPayload) => { - const res = await client.post>( + const res = await client.post( URL_CONSTANTS.AUTH.LOGIN, body, ); - return res.data.data; + return res.data; }, createUser: async (body: SignupPayload) => { - const res = await client.post>( + const res = await client.post> ( URL_CONSTANTS.USERS.SIGN_UP, body, ); - return res.data.data; + return res.data; }, getMyInfo: async () => { - const res = await client.get>( + const res = await client.get( URL_CONSTANTS.USERS.ME, ); - return res.data.data; + return res.data; }, generateVerificationCode: async (body: GenerateVerificationCodePayload) => { From 10d74a65b04ac3f9a61b686509c1c5e5007aac38 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 23 Jun 2026 14:20:16 +0000 Subject: [PATCH 3/3] fix: type error and fixes --- .../portal/src/components/PhoneField.tsx | 21 +- .../onboarding/OnboardingWizardDialog.tsx | 242 ++++++-- .../portal/src/components/phone-field.css | 6 +- .../src/pages/accounts/CompanyProfileForm.tsx | 80 --- .../src/pages/accounts/ForwarderForm.tsx | 580 ------------------ .../src/pages/accounts/OnboardingPage.tsx | 300 --------- .../src/pages/settings/NationalitySelect.tsx | 42 +- .../pages/settings/OnboardingRoleSelect.tsx | 34 +- .../src/pages/settings/TabCompanyProfile.tsx | 45 +- 9 files changed, 254 insertions(+), 1096 deletions(-) delete mode 100644 apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx delete mode 100644 apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx diff --git a/apps/edr-freight-web/portal/src/components/PhoneField.tsx b/apps/edr-freight-web/portal/src/components/PhoneField.tsx index a50621e60..abc7083b5 100644 --- a/apps/edr-freight-web/portal/src/components/PhoneField.tsx +++ b/apps/edr-freight-web/portal/src/components/PhoneField.tsx @@ -1,5 +1,4 @@ -import { Input } from "@mantine/core"; -import { forwardRef } from "react"; +import { Input, TextInput } from "@mantine/core"; import { Controller, type Control, @@ -32,17 +31,6 @@ export const toEthiopianE164 = (raw?: string | null): string => { return `+251${digits}`; }; -/** - * The text input rendered inside react-phone-number-input, styled to match the - * portal's Mantine fields (44px height, 10px radius, edr border). Must forward - * the ref and accept native input props for the library to drive it. - */ -const StyledInput = forwardRef>( - function StyledInput(props, ref) { - return ; - }, -); - export interface PhoneFieldProps { label?: string; value?: string; @@ -75,10 +63,10 @@ export function PhoneField({ required={required} error={error} styles={{ - label: { fontWeight: 600, fontSize: 13, color: "#10202F", marginBottom: 6 }, + label: { fontWeight: 600, fontSize: 14, color: "#10202F", }, }} > -
+
diff --git a/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx index 17fde6528..3628d7403 100644 --- a/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx +++ b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx @@ -1,8 +1,33 @@ -import { Modal, ScrollArea, Stack, Text } from "@mantine/core"; +import { + Box, + Button, + Group, + Modal, + ScrollArea, + Stack, + Text, + Title, +} from "@mantine/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + ArrowLeft, + ArrowRight, + Building2, + CheckCircle2, + FileText, + Globe2, + UploadCloud, + User, + UserCheck, +} from "lucide-react"; +import type { ReactNode } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; +import type { RoleLicenseProfile } from "@/components/onboarding/RoleLicenseStep"; import useAuth from "@/hooks/useAuth"; +import CompanyProfileForm from "@/pages/accounts/CompanyProfileForm"; +import NationalitySelect from "@/pages/settings/NationalitySelect"; +import OnboardingRoleSelect from "@/pages/settings/OnboardingRoleSelect"; import { api } from "@/services/api"; import type { CompanyNationality, @@ -12,12 +37,8 @@ import type { import { companiesService } from "@/services/companies.service"; import type { UpdateProfilePayload } from "@/types/profile"; import { extractApiError } from "@/utils/result"; -import CompanyProfileForm from "@/pages/accounts/CompanyProfileForm"; -import type { RoleLicenseProfile } from "@/components/onboarding/RoleLicenseStep"; -import NationalitySelect from "@/pages/settings/NationalitySelect"; -import OnboardingRoleSelect from "@/pages/settings/OnboardingRoleSelect"; -/** Form steps shared by CompanyProfileForm and ForwarderForm. */ +/** Form steps rendered by CompanyProfileForm. */ type FormStep = | "company" | "personnel" @@ -34,6 +55,58 @@ const FORM_STEPS: FormStep[] = [ "additional", ]; +/** The full onboarding journey: the two pre-form phases + the form steps. */ +type WizardStep = "nationality" | "role" | FormStep; +const WIZARD_STEPS: WizardStep[] = ["nationality", "role", ...FORM_STEPS]; + +/** Icon + title + description shown in the global dialog header per step. */ +const STEP_META: Record< + WizardStep, + { icon: ReactNode; title: string; description: string } +> = { + nationality: { + icon: , + title: "Where is your company registered?", + description: "This determines the documents we'll ask you to provide.", + }, + role: { + icon: , + title: "What does your company do?", + description: + "Pick any combination of Importer, Exporter and Freight Forwarder — each is set up with its own business license.", + }, + company: { + icon: , + title: "Company Information", + description: "Tell us about your company and its registration details.", + }, + personnel: { + icon: , + title: "General Manager", + description: "Who is the general manager of the company?", + }, + contact: { + icon: , + title: "Contact Person", + description: "Who should we reach out to about this account?", + }, + poa: { + icon: , + title: "Power of Attorney", + description: "Optionally add a representative with power of attorney.", + }, + documents: { + icon: , + title: "Upload Documents", + description: "Provide the required company documents.", + }, + additional: { + icon: , + title: "Business License", + description: "Upload a business license for each operational profile.", + }, +}; + interface OnboardingWizardDialogProps { opened: boolean; /** Dismiss the dialog (user clicked the close icon). */ @@ -98,6 +171,9 @@ export default function OnboardingWizardDialog({ // Newly-selected business-license files per company_profile id. const [licenseFiles, setLicenseFiles] = useState>({}); const [startError, setStartError] = useState(null); + // Mirror of CompanyProfileForm's active step so the global header + progress + // pill can reflect it (the form no longer renders its own stepper). + const [formStep, setFormStep] = useState(resumeFormStep); // Saved profile data, for rehydrating the form fields after a refresh. const profileQuery = useQuery( @@ -164,6 +240,15 @@ export default function OnboardingWizardDialog({ api.companies.setOnboardingStep.call({ step }).catch(() => {}); }, []); + // Mirror the form's step locally (for the header/pill) and persist it. + const handleStepChange = useCallback( + (step: string) => { + setFormStep(step as FormStep); + persistStep(step); + }, + [persistStep], + ); + // The company query may resolve AFTER this dialog mounts (it's kept mounted by // the gate), so the phase/roles/nationality initial state can be stale — a // draft that already exists would otherwise leave us stuck on the first @@ -239,12 +324,10 @@ export default function OnboardingWizardDialog({ existingFiles: p.licenseFiles ?? [], })); - const titleHint = - phase === "nationality" - ? "Where is your company registered?" - : phase === "role" - ? "Tell us what your company does to get started." - : "Set up your company profile to finish."; + // The active step across the whole journey, driving the header + progress pill. + const activeStep: WizardStep = phase === "form" ? formStep : phase; + const stepMeta = STEP_META[activeStep]; + const activeIdx = WIZARD_STEPS.indexOf(activeStep); const formProps = { documentSettingCode: documentSettingCode(effectiveNationality), @@ -257,7 +340,7 @@ export default function OnboardingWizardDialog({ hideFirstStepBack: true, initialStep: resumeFormStep, resyncOpen: opened, - onStepChange: persistStep, + onStepChange: handleStepChange, onSaveStep: saveStep, rehydrate: profileQuery.data ?? null, roleProfiles, @@ -279,63 +362,98 @@ export default function OnboardingWizardDialog({ keepMounted scrollAreaComponent={ScrollArea.Autosize} overlayProps={{ backgroundOpacity: 0.55, blur: 4 }} + styles={{ + header: { + alignItems:"flex-start" + }, + title: { + flex: 1 + } + }} title={ - - - Complete your onboarding - - - {titleHint} - + + + + {stepMeta.icon} + {stepMeta.title} + + + {stepMeta.description} + + + } > - {phase === "nationality" ? ( - - - - - ) : phase === "role" ? ( - - - {startError && ( - - {startError} - - )} - - - ) : ( - - )} + + + {phase === "nationality" ? ( + + + + + + + ) : phase === "role" ? ( + + + {startError && ( + + {startError} + + )} + + + + + + ) : ( + + )} + ); } -function RoleContinueBar({ - disabled, - loading, - onClick, -}: { - disabled: boolean; - loading?: boolean; - onClick: () => void; -}) { +/** + * Continuous progress pill: a single rounded track that fills left-to-right as + * the user advances, with faint ticks marking each step boundary. + */ +function ProgressPill({ current, total }: { current: number; total: number }) { + const pct = total > 0 ? ((current + 1) / total) * 100 : 0; return ( - + + + ); } diff --git a/apps/edr-freight-web/portal/src/components/phone-field.css b/apps/edr-freight-web/portal/src/components/phone-field.css index 2fd037b99..3a0396ffd 100644 --- a/apps/edr-freight-web/portal/src/components/phone-field.css +++ b/apps/edr-freight-web/portal/src/components/phone-field.css @@ -11,9 +11,9 @@ .edr-phone-wrapper .PhoneInputCountry { margin: 0; padding: 0 10px; - height: 44px; - border: 1px solid #e6ecf2; - border-radius: 10px; + height: 2.25rem; + border: 0.0625rem solid #b0bfce; + border-radius: 6px; background: #fff; display: flex; align-items: center; diff --git a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx index 29055a2e8..115351440 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -1,6 +1,5 @@ import { Alert, - Box, Button, Checkbox, Divider, @@ -10,7 +9,6 @@ import { Stack, Text, TextInput, - ThemeIcon, } from "@mantine/core"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; @@ -18,12 +16,6 @@ import { AlertCircle, ArrowLeft, ArrowRight, - Building2, - CheckCircle2, - ChevronLeft, - FileText, - UploadCloud, - User, UserCheck, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; @@ -495,7 +487,6 @@ export default function CompanyProfileForm({ "documents", "additional", ]; - const totalSteps = stepOrder.length; const currentIdx = stepOrder.indexOf(step); /** Validate + persist the current step, returning whether we may advance. */ @@ -553,79 +544,8 @@ export default function CompanyProfileForm({ // selection); otherwise always available. const showBack = !(hideFirstStepBack && step === "company"); - const STEP_ICONS: Record = { - company: , - personnel: , - contact: , - poa: , - documents: , - additional: , - }; - - const STEP_TITLES: Record = { - company: "Company Information", - personnel: "General Manager", - contact: "Contact Person", - poa: "Power of Attorney (Optional)", - documents: "Upload Documents", - additional: "Business License", - }; - - const stepLabel = `Step ${currentIdx + 1} of ${totalSteps} — ${STEP_TITLES[step]}`; - return ( <> - - - - - - {stepOrder.map((key, i) => { - const done = i < currentIdx; - const active = i === currentIdx; - return done || active ? ( - - {done ? : STEP_ICONS[key]} - - ) : ( - - {STEP_ICONS[key]} - - ); - })} - - - - {stepLabel} - - - e.preventDefault()}> {step === "company" && ( diff --git a/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx deleted file mode 100644 index e13ec2282..000000000 --- a/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx +++ /dev/null @@ -1,580 +0,0 @@ -import { Alert, Box, Button, Divider, Group, Loader, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useQuery } from "@tanstack/react-query"; -import { - AlertCircle, - ArrowLeft, - ArrowRight, - Building2, - CheckCircle2, - ChevronLeft, - FileText, - UploadCloud, - User, -} from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import type { AuthUser } from "@/types/auth"; -import type { CreateCompanyPayload } from "@/services/companies.service"; -import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; -import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField"; -import { SmartFileInput } from "@edr/ui-common"; -import { api } from "@/services/api"; -import RoleLicenseStep, { - type RoleLicenseProfile, -} from "@/components/onboarding/RoleLicenseStep"; - -type ForwarderStep = "company" | "personnel" | "poa" | "documents" | "additional"; - -const forwarderSchema = z.object({ - companyName: z.string().min(1, "Company name is required"), - companyEmail: z.string().email("Invalid email address"), - companyPhone: z - .string() - .min(1, "Company phone is required") - .refine(isValidPhone, "Enter a valid phone number"), - companyLocation: z.string().min(1, "Location is required"), - companyAddress: z.string().min(1, "Address is required"), - tinNumber: z.string().length(10, "TIN must be exactly 10 digits"), - vatNumber: z.string().min(1, "VAT number is required").length(10, "VAT number must be exactly 10 digits"), - fanNumber: z.string().length(16, "FAN must be exactly 16 digits"), - contactPersonName: z.string().min(1, "Contact person name is required"), - contactPersonPhone: z - .string() - .min(1, "Contact person phone is required") - .refine(isValidPhone, "Enter a valid phone number"), - generalManagerName: z.string().min(1, "GM name is required"), - generalManagerEmail: z.string().email("Invalid GM email"), - generalManagerPhone: z - .string() - .min(1, "GM phone is required") - .refine(isValidPhone, "Enter a valid phone number"), - poaName: z.string().optional(), - poaPhone: z - .string() - .optional() - .refine((v) => !v || isValidPhone(v), "Enter a valid phone number"), - poaAddress: z.string().optional(), - poaEmail: z.string().optional(), - poaLocation: z.string().optional(), -}); - -type FormData = z.infer; - -const stepFields: Record = { - company: ["companyName", "companyEmail", "companyPhone", "companyLocation", "companyAddress", "tinNumber", "vatNumber", "fanNumber"], - personnel: ["contactPersonName", "contactPersonPhone", "generalManagerName", "generalManagerEmail", "generalManagerPhone"], - poa: [], - documents: [], - additional: [], -}; - -function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload { - return { - companyName: data.companyName, - companyEmail: data.companyEmail, - companyPhone: data.companyPhone, - companyLocation: data.companyLocation, - companyAddress: data.companyAddress, - tin: data.tinNumber, - vatNumber: data.vatNumber, - fanNumber: data.fanNumber, - attributes: { - contactPersonName: data.contactPersonName, - contactPersonPhone: data.contactPersonPhone, - generalManagerName: data.generalManagerName, - generalManagerEmail: data.generalManagerEmail, - generalManagerPhone: data.generalManagerPhone, - poaName: data.poaName || undefined, - poaPhone: data.poaPhone || undefined, - poaAddress: data.poaAddress || undefined, - poaEmail: data.poaEmail || undefined, - poaLocation: data.poaLocation || undefined, - }, - }; -} - -/** Map one wizard step's form values to the profile-update payload it saves. */ -function stepPayload(step: ForwarderStep, d: FormData): Partial { - switch (step) { - case "company": - return { - companyName: d.companyName, - companyEmail: d.companyEmail, - companyPhone: d.companyPhone, - companyLocation: d.companyLocation, - companyAddress: d.companyAddress, - tin: d.tinNumber, - vatNumber: d.vatNumber, - fanNumber: d.fanNumber, - }; - case "personnel": - return { - contactPersonName: d.contactPersonName, - contactPersonPhone: d.contactPersonPhone, - generalManagerName: d.generalManagerName, - generalManagerEmail: d.generalManagerEmail, - generalManagerPhone: d.generalManagerPhone, - }; - case "poa": - return { - poaName: d.poaName || undefined, - poaPhone: d.poaPhone || undefined, - poaEmail: d.poaEmail || undefined, - poaLocation: d.poaLocation || undefined, - poaAddress: d.poaAddress || undefined, - }; - default: - return {}; - } -} - -/** Seed the form from previously-saved profile data. */ -function toFormValues(p: ProfileResponse): FormData { - const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : ""; - return { - companyName: p.companyName ?? "", - companyEmail: p.companyEmail ?? "", - companyPhone: p.companyPhone ?? "", - companyLocation: p.companyLocation ?? "", - companyAddress: p.companyAddress ?? "", - tinNumber: tin, - vatNumber: p.vatNumber ?? "", - fanNumber: p.fanNumber ?? "", - contactPersonName: p.contactPersonName ?? "", - contactPersonPhone: p.contactPersonPhone ?? "", - generalManagerName: p.generalManagerName ?? "", - generalManagerEmail: p.generalManagerEmail ?? "", - generalManagerPhone: p.generalManagerPhone ?? "", - poaName: p.poaName ?? "", - poaPhone: p.poaPhone ?? "", - poaAddress: p.poaAddress ?? "", - poaEmail: p.poaEmail ?? "", - poaLocation: p.poaLocation ?? "", - }; -} - -export default function ForwarderForm({ - documentSettingCode, - documentFiles: controlledFiles, - onDocumentFilesChange, - user, - onSubmit, - isPending, - onBack, - initialStep, - resyncOpen, - hideFirstStepBack, - onStepChange, - onSaveStep, - rehydrate, - roleProfiles, - licenseFiles, - onLicenseChange, -}: { - documentSettingCode: string; - documentFiles?: Record; - onDocumentFilesChange?: (files: Record) => void; - user: AuthUser; - onSubmit: (data: CreateCompanyPayload) => void; - isPending: boolean; - onBack: () => void; - /** Step to resume at (defaults to "company"). */ - initialStep?: ForwarderStep; - /** When this flips true (dialog reopened), jump back to initialStep (furthest reached). */ - resyncOpen?: boolean; - /** Hide the Back button on the first step (onboarding can't go back to role pick). */ - hideFirstStepBack?: boolean; - /** Reports the active step so the parent can persist resume progress. */ - onStepChange?: (step: ForwarderStep) => void; - /** Persist the current step's data before advancing; returns an error to show. */ - onSaveStep?: ( - data: Partial, - ) => Promise<{ ok: true } | { ok: false; error: string }>; - /** Saved profile to seed the form with (rehydration after refresh). */ - rehydrate?: ProfileResponse | null; - /** Operational profiles for the final per-role license step. */ - roleProfiles?: RoleLicenseProfile[]; - /** Newly-selected license files per profile id. */ - licenseFiles?: Record; - onLicenseChange?: (value: Record) => void; -}) { - const [step, setStep] = useState(initialStep ?? "company"); - const [saving, setSaving] = useState(false); - const [saveError, setSaveError] = useState(null); - - // Report each step change up so the wizard can persist it for resume. - useEffect(() => { - onStepChange?.(step); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [step]); - - // On reopen, jump to the furthest step reached so progress never resets. - const wasOpen = useRef(resyncOpen); - useEffect(() => { - if (resyncOpen && !wasOpen.current && initialStep) { - setStep(initialStep); - setSaveError(null); - } - wasOpen.current = resyncOpen; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [resyncOpen]); - const [internalFiles, setInternalFiles] = useState>({}); - const documentFiles = controlledFiles ?? internalFiles; - const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles; - - const { data: uploadSetting, isLoading: loadingDocuments } = useQuery( - api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }), - ); - - const { register, control, handleSubmit, trigger, watch, formState: { errors } } = useForm({ - resolver: zodResolver(forwarderSchema), - defaultValues: { - companyName: "", companyEmail: "", companyPhone: "", - companyLocation: "", companyAddress: "", tinNumber: "", vatNumber: "", fanNumber: "", - contactPersonName: "", contactPersonPhone: "", - generalManagerName: "", generalManagerEmail: "", generalManagerPhone: "", - poaName: "", poaPhone: "", poaAddress: "", poaEmail: "", poaLocation: "", - }, - // Rehydrate from previously-saved data (RHF re-syncs when `values` change). - values: rehydrate ? toFormValues(rehydrate) : undefined, - }); - - const hasDocuments = Boolean(uploadSetting?.fields?.length); - const totalSteps = 5; - - /** Validate + persist the current step, returning whether we may advance. */ - const saveCurrentStep = async (): Promise => { - setSaveError(null); - const isValid = await trigger(stepFields[step]); - if (!isValid) return false; - if (!onSaveStep) return true; - setSaving(true); - try { - const res = await onSaveStep(stepPayload(step, watch())); - if (!res.ok) { - setSaveError(res.error); - return false; - } - return true; - } finally { - setSaving(false); - } - }; - - // Every role needs at least one license file (existing or newly selected). - const licenseComplete = (roleProfiles ?? []).every( - (p) => - (licenseFiles?.[p.id]?.length ?? 0) > 0 || p.existingFiles.length > 0, - ); - - const nextStep = async () => { - if (step === "additional") { - if (!licenseComplete) { - setSaveError( - "Please upload a business license for each of your operational profiles.", - ); - return; - } - handleSubmit((data) => onSubmit(buildPayload(data, user)))(); - return; - } - if (step === "documents") { setStep("additional"); return; } - const ok = await saveCurrentStep(); - if (!ok) return; - setStep(step === "company" ? "personnel" : step === "personnel" ? "poa" : "documents"); - }; - - const skipDocuments = () => setStep("additional"); - - const prevStep = () => { - setSaveError(null); - if (step === "company") onBack(); - else if (step === "personnel") setStep("company"); - else if (step === "poa") setStep("personnel"); - else if (step === "documents") setStep("poa"); - else setStep("documents"); - }; - - const showBack = !(hideFirstStepBack && step === "company"); - - const STEPS: { key: ForwarderStep; icon: React.ReactNode }[] = [ - { key: "company", icon: }, - { key: "personnel", icon: }, - { key: "poa", icon: }, - { key: "documents", icon: }, - { key: "additional", icon: }, - ]; - - const STEP_LABELS: Record = { - company: `Step 1 of ${totalSteps} — Company Information`, - personnel: `Step 2 of ${totalSteps} — Personnel Details`, - poa: `Step 3 of ${totalSteps} — Power of Attorney (Optional)`, - documents: `Step 4 of ${totalSteps} — Upload Documents`, - additional: `Step 5 of ${totalSteps} — Business License`, - }; - - const stepOrder: ForwarderStep[] = ["company", "personnel", "poa", "documents", "additional"]; - const currentIdx = stepOrder.indexOf(step); - - return ( - <> - - - - - - {STEPS.map(({ key, icon }, i) => { - const done = i < currentIdx; - const active = i === currentIdx; - return done || active ? ( - - {done ? : icon} - - ) : ( - - {icon} - - ); - })} - - - - {STEP_LABELS[step]} - - - - e.preventDefault()}> - - {step === "company" && ( - <> - - - - - - - - - - - - - - - - )} - - {step === "personnel" && ( - <> - Contact Person - - - - - - - - General Manager - - - - - - - )} - - {step === "poa" && ( - <> - - Power of Attorney details are optional. Fill them in if you have them, or skip to continue. - - - - - - - - - - - - )} - - {step === "documents" && ( - <> - {loadingDocuments ? ( - - - - ) : !uploadSetting ? ( - - No document requirements found for your account type. - - ) : ( - - )} - - )} - - {step === "additional" && ( - {})} - /> - )} - - {saveError && ( - } - title={step === "additional" ? "Business license required" : "Couldn't save this step"} - > - {saveError} - - )} - - - {showBack ? ( - - ) : ( - - )} - - {step === "documents" && ( - - )} - - - - - - - ); -} diff --git a/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx deleted file mode 100644 index 861fbbb75..000000000 --- a/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import { - Box, - Group, - SimpleGrid, - Stack, - Text, - ThemeIcon, - UnstyledButton, -} from "@mantine/core"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { - ArrowDownToLine, - ArrowUpFromLine, - Building2, - ChevronRight, -} from "lucide-react"; -import { useState } from "react"; - -import AuthLayout from "@/components/auth/AuthLayout"; -import useAuth from "@/hooks/useAuth"; -import { api } from "@/services/api"; -import type { CreateCompanyPayload } from "@/services/companies.service"; -import { companiesService } from "@/services/companies.service"; -import CompanyProfileForm from "./CompanyProfileForm"; -import DjiboutiAgentForm from "./DjiboutiAgentForm"; -import ForwarderForm from "./ForwarderForm"; -import TransporterForm from "./TransporterForm"; -import type { OnboardingUserType } from "./types"; - -const USER_TYPE_CARDS: { - id: OnboardingUserType; - label: string; - description: string; - icon: React.ReactNode; -}[] = [ - { - id: "importer", - label: "Importer", - description: "Import goods into Ethiopia via the railway corridor.", - icon: , - }, - { - id: "exporter", - label: "Exporter", - description: "Export goods from Ethiopia via rail.", - icon: , - }, - { - id: "freight-forwarder-et", - label: "Freight Forwarder (Ethiopia)", - description: "Ethiopian freight forwarding company handling client cargo.", - icon: , - }, - // { - // id: "freight-forwarder-dj", - // label: "FF Agent (Djibouti)", - // description: "Djibouti-based agent coordinating cross-border logistics.", - // icon: , - // }, - // { - // id: "transporter", - // label: "Transporter", - // description: "Trucking company providing first/last-mile services.", - // icon: , - // }, - ]; - -const USER_TYPE_LEFT_MAP: Record< - OnboardingUserType, - { badge: string; title: string; description: string } -> = { - importer: { - badge: "Importer Registration", - title: "Register as an Importer", - description: - "Set up your company profile to manage imports, track shipments, and streamline customs clearance across the Ethiopia-Djibouti corridor.", - }, - exporter: { - badge: "Exporter Registration", - title: "Register as an Exporter", - description: - "Set up your company profile to manage exports, coordinate outbound logistics, and access rail transport services.", - }, - "freight-forwarder-et": { - badge: "Freight Forwarder Registration (Ethiopia)", - title: "Register Your Forwarding Company", - description: - "Complete your company profile and Power of Attorney to handle cargo on behalf of importers and exporters.", - }, - "freight-forwarder-dj": { - badge: "FF Agent Registration (Djibouti)", - title: "Register as a Djibouti Agent", - description: - "Register your company details and representative information to coordinate cross-border freight operations.", - }, - transporter: { - badge: "Transporter Registration", - title: "Register Your Transport Services", - description: - "Provide your vehicle and fleet details to offer first-mile and last-mile trucking services integrated with rail.", - }, -}; - -const PREFLIGHT_LEFT = { - badge: "Get Started", - title: "Choose your account type", - description: - "Select the profile that best matches your role in the logistics chain. Each account type provides a tailored onboarding experience.", - features: [ - "Importers & Exporters", - "Freight Forwarders (Ethiopia & Djibouti)", - "Transporters & Fleet Operators", - ], - stats: { - label: "Active Customers", - value: "500+", - footer: "And growing", - progress: "w-[95%]", - }, -}; - -const DOCUMENT_SETTING_CODE_MAP: Record = { - importer: "company_onboarding_documents_customer", - exporter: "company_onboarding_documents_customer", - "freight-forwarder-et": "company_onboarding_documents_forwarder", - "freight-forwarder-dj": "company_onboarding_documents_forwarder_dj", - transporter: "company_onboarding_documents_transporter", -}; - -export default function OnboardingPage() { - const queryClient = useQueryClient(); - const { user } = useAuth(); - const [userType, setUserType] = useState(null); - const [documentFiles, setDocumentFiles] = useState< - Record - >({}); - - const COMPANY_TYPE_MAP: Record = { - importer: "customer", - exporter: "customer", - "freight-forwarder-et": "forwarder", - "freight-forwarder-dj": "forwarder", - transporter: "transporter", - }; - - const createCompanyMutation = useMutation({ - mutationFn: (payload: CreateCompanyPayload) => - api.companies.create.call(payload), - onSuccess: async (data) => { - const hasFiles = Object.values(documentFiles).some( - (f) => f !== null && (Array.isArray(f) ? f.length > 0 : true), - ); - if (hasFiles) { - await companiesService.uploadDocuments(data.company.id, documentFiles); - } - await queryClient.invalidateQueries({ - queryKey: api.companies.getInfo.queryKey(), - }); - }, - }); - - if (!user) return null; - - const handleSubmit = (payload: CreateCompanyPayload) => { - const enriched: CreateCompanyPayload = { - ...payload, - companyType: COMPANY_TYPE_MAP[userType!], - }; - createCompanyMutation.mutate(enriched); - }; - - const handleSelectType = (type: OnboardingUserType) => setUserType(type); - const handleBack = () => setUserType(null); - - if (!userType) { - return ( - - - - - Select Account Type - - - Choose the account type that fits your role. - - - - - {USER_TYPE_CARDS.map((card) => ( - handleSelectType(card.id)} - className="group block rounded-lg shadow-lg! border! border-edr-border! bg-edr-card! p-5! text-left transition-all duration-200 hover:-translate-y-0.5 hover:border-[var(--mantine-color-edr-green-5)]! hover:bg-edr-soft hover:shadow-[0_12px_28px_-14px_rgba(14,163,83,0.55)]" - > - - - {card.icon} - - - - {card.label} - - - {card.description} - - - - - - ))} - - - - ); - } - - const leftConfig = USER_TYPE_LEFT_MAP[userType]; - const leftProps = { - ...leftConfig, - features: - userType === "transporter" - ? [ - "Vehicle & fleet registration", - "TIN & FAN verification", - "First-mile / Last-mile eligibility", - ] - : userType === "freight-forwarder-dj" - ? [ - "Company details", - "Representative information", - "Cross-border operations", - ] - : [ - "Company registration details", - "Contact and management personnel", - "Power of Attorney (optional)", - ], - stats: { - label: "Active Customers", - value: "500+", - footer: "And growing", - progress: "w-[95%]", - }, - }; - - return ( - - {userType === "transporter" ? ( - - ) : userType === "freight-forwarder-dj" ? ( - - ) : userType === "freight-forwarder-et" ? ( - - ) : ( - - )} - - ); -} diff --git a/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx b/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx index 1d1dbb5c4..5b779847a 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx @@ -7,6 +7,8 @@ import RoleCard from "./RoleCard"; interface NationalitySelectProps { value: CompanyNationality | null; onChange: (next: CompanyNationality) => void; + /** Render only the option grid — the wizard supplies its own header/card. */ + embedded?: boolean; } /** @@ -18,7 +20,29 @@ interface NationalitySelectProps { export default function NationalitySelect({ value, onChange, + embedded = false, }: NationalitySelectProps) { + const grid = ( + + } + selected={value === "ethiopian"} + onClick={() => onChange("ethiopian")} + /> + } + selected={value === "foreign"} + onClick={() => onChange("foreign")} + /> + + ); + + if (embedded) return grid; + return ( @@ -28,23 +52,7 @@ export default function NationalitySelect({ This determines the documents we'll ask you to provide. - - - } - selected={value === "ethiopian"} - onClick={() => onChange("ethiopian")} - /> - } - selected={value === "foreign"} - onClick={() => onChange("foreign")} - /> - + {grid} ); } diff --git a/apps/edr-freight-web/portal/src/pages/settings/OnboardingRoleSelect.tsx b/apps/edr-freight-web/portal/src/pages/settings/OnboardingRoleSelect.tsx index d5196d024..ca4afb9b9 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/OnboardingRoleSelect.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/OnboardingRoleSelect.tsx @@ -7,6 +7,8 @@ interface OnboardingRoleSelectProps { /** Currently selected profile types (e.g. ["importer"], ["importer","exporter","freight_forwarder"]). */ value: string[]; onChange: (next: string[]) => void; + /** Render only the option grid — the wizard supplies its own header/card. */ + embedded?: boolean; } /** @@ -19,6 +21,7 @@ interface OnboardingRoleSelectProps { export default function OnboardingRoleSelect({ value, onChange, + embedded = false, }: OnboardingRoleSelectProps) { const selected = new Set(value); @@ -29,6 +32,23 @@ export default function OnboardingRoleSelect({ onChange([...next]); }; + const grid = ( + + {CUSTOMER_ROLES.map((role) => ( + toggleRole(role.type)} + /> + ))} + + ); + + if (embedded) return grid; + return ( @@ -39,19 +59,7 @@ export default function OnboardingRoleSelect({ Pick any combination of Importer, Exporter and Freight Forwarder — each is set up with its own business license. - - - {CUSTOMER_ROLES.map((role) => ( - toggleRole(role.type)} - /> - ))} - + {grid} ); } diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx index 1f49baf7c..995acc104 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx @@ -1,27 +1,26 @@ -import { useMemo, useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { Building2, CheckCircle2, Save, XCircle } from "lucide-react"; -import { - Card, - Group, - Stack, - Title, - Text, - TextInput, - Button, - Grid, -} from "@mantine/core"; -import { api } from "@/services/api"; import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField"; -import type { ProfileResponse } from "@/types/profile"; +import { api } from "@/services/api"; import type { - CreateCompanyPayload, - CompanyProfileInput, + CompanyProfileInput, + CreateCompanyPayload, } from "@/services/companies.service"; -import CompanyRolesCard from "./CompanyRolesCard"; +import type { ProfileResponse } from "@/types/profile"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Button, + Card, + Grid, + Group, + Stack, + Text, + TextInput, + Title, +} from "@mantine/core"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Building2, CheckCircle2, Save, XCircle } from "lucide-react"; +import { useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; import OnboardingRoleSelect from "./OnboardingRoleSelect"; export const COMPANY_PROFILE_SCHEMA = z.object({ @@ -143,9 +142,7 @@ export default function TabCompanyProfile({ value={selectedRoles} onChange={setSelectedRoles} /> - ) : ( - profile && - )} + ) : null} {showForm && (