From d52a5715e08d07451de72ccbad04ab663cc8efc4 Mon Sep 17 00:00:00 2001 From: hagiye Date: Tue, 2 Jun 2026 10:47:36 +0300 Subject: [PATCH] Rate Matrix Rules user story --- apps/edr-freight-web/backoffice/package.json | 1 + .../backoffice/src/auth/types.ts | 7 + .../backoffice/src/auth/useAuth.ts | 2 + .../baselineRatematrix/RateMatrixForm.tsx | 330 ++++++++++++++++++ .../components/ruleEngine/rateMatrixRules.ts | 173 +++++++++ .../src/constants/TANSTACK_QUEY_KEY.ts | 20 +- .../backoffice/src/constants/URLS.ts | 14 + .../src/constants/rateMatrixConstants.ts | 60 ++++ .../admin/rateMatrix/RateMatrixApproval.tsx | 123 +++++++ .../rateMatrix/RateMatrixRegistration.tsx | 25 ++ apps/edr-freight-web/backoffice/tsconfig.json | 14 +- pnpm-lock.yaml | 3 + 12 files changed, 769 insertions(+), 3 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/ruleEngine/rateMatrixRules.ts create mode 100644 apps/edr-freight-web/backoffice/src/constants/rateMatrixConstants.ts create mode 100644 apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx create mode 100644 apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx diff --git a/apps/edr-freight-web/backoffice/package.json b/apps/edr-freight-web/backoffice/package.json index f39ee4541..7499e38a2 100644 --- a/apps/edr-freight-web/backoffice/package.json +++ b/apps/edr-freight-web/backoffice/package.json @@ -27,6 +27,7 @@ "react-hot-toast": "^2.6.0", "react-router-dom": "^6.27.0", "recharts": "^3.8.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "zustand": "^5.0.0" }, diff --git a/apps/edr-freight-web/backoffice/src/auth/types.ts b/apps/edr-freight-web/backoffice/src/auth/types.ts index 2d4ecd536..5efcdd828 100644 --- a/apps/edr-freight-web/backoffice/src/auth/types.ts +++ b/apps/edr-freight-web/backoffice/src/auth/types.ts @@ -52,3 +52,10 @@ export interface AuthTokens { export interface LoginResponse extends Partial { mfaRequired?: boolean; } + +// Additional types for Matrix form test +export interface User { + id: string; + name: string; + role: "ADMIN" | "MANAGER" | "CHIEF_EXECUTIVE"; +} diff --git a/apps/edr-freight-web/backoffice/src/auth/useAuth.ts b/apps/edr-freight-web/backoffice/src/auth/useAuth.ts index e454bc509..74a29cf7e 100644 --- a/apps/edr-freight-web/backoffice/src/auth/useAuth.ts +++ b/apps/edr-freight-web/backoffice/src/auth/useAuth.ts @@ -11,3 +11,5 @@ export const useAuth = () => { return context; }; + + diff --git a/apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx b/apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx new file mode 100644 index 000000000..f18389f9e --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/baselineRatematrix/RateMatrixForm.tsx @@ -0,0 +1,330 @@ +// 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/ruleEngine/rateMatrixRules.ts b/apps/edr-freight-web/backoffice/src/components/ruleEngine/rateMatrixRules.ts new file mode 100644 index 000000000..f0080c5a3 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/ruleEngine/rateMatrixRules.ts @@ -0,0 +1,173 @@ +// ruleEngine/rateMatrixRules.ts +import { RATE_TYPES, REQUIRED_RATE_TYPES, MATRIX_STATUS } from '@/constants/rateMatrixConstants'; + +interface RateEntry { + rateType: string; + entries: Array>; +} + +interface ValidationRule { + id: string; + description: string; + severity: 'error' | 'warning'; + validate: (data: any) => boolean; + message: string; +} + +export class RateMatrixRulesEngine { + private rules: ValidationRule[] = []; + + constructor() { + this.initializeRules(); + } + + private initializeRules() { + // Rule 1: All rate types must be present + this.rules.push({ + id: 'ALL_TYPES_REQUIRED', + description: 'Verify all 13 rate types are included', + severity: 'error', + validate: (rateSections: RateEntry[]) => { + const submittedTypes = rateSections.map(s => s.rateType); + return REQUIRED_RATE_TYPES.every(type => submittedTypes.includes(type)); + }, + message: 'All 13 rate types must be included in the submission', + }); + + // Rule 2: Each rate type must have at least one entry + this.rules.push({ + id: 'MINIMUM_ENTRIES', + description: 'Each rate type requires at least one rate entry', + severity: 'error', + validate: (rateSections: RateEntry[]) => { + return rateSections.every(section => section.entries.length > 0); + }, + message: 'Each rate type must have at least one rate entry', + }); + + // Rule 3: Dates must be valid + this.rules.push({ + id: 'VALID_DATES', + description: 'Rate entries must have valid date ranges', + severity: 'error', + validate: (rateSections: RateEntry[]) => { + return rateSections.every(section => + section.entries.every(entry => { + if (!entry.validFrom) return false; + if (entry.validTo && new Date(entry.validTo) <= new Date(entry.validFrom)) { + return false; + } + return true; + }) + ); + }, + message: 'All rate entries must have valid dates (Valid To must be after Valid From)', + }); + + // Rule 4: Rates must be non-negative + this.rules.push({ + id: 'NON_NEGATIVE_RATES', + description: 'All rate values must be non-negative', + severity: 'error', + validate: (rateSections: RateEntry[]) => { + const numericFields = ['baseRate', 'ratePerMetricTon', 'ratePerKm', + 'ratePerTrip', 'ratePerDay', 'ratePerUnit']; + + return rateSections.every(section => + section.entries.every(entry => { + return numericFields.every(field => { + const value = entry[field]; + return value === undefined || value === '' || Number(value) >= 0; + }); + }) + ); + }, + message: 'Rate values cannot be negative', + }); + + // Rule 5: Business rule - Demurrage free days should be reasonable + this.rules.push({ + id: 'DEMURRAGE_FREE_DAYS', + description: 'Demurrage free days should be between 0 and 30', + severity: 'warning', + validate: (rateSections: RateEntry[]) => { + const demurrageSection = rateSections.find( + s => s.rateType === RATE_TYPES.DEMURRAGE + ); + if (!demurrageSection) return true; + + return demurrageSection.entries.every(entry => { + const freeDays = Number(entry.freeDays); + return !freeDays || (freeDays >= 0 && freeDays <= 30); + }); + }, + message: 'Demurrage free days typically range from 0 to 30 days', + }); + + // Rule 6: Cancellation fee percentage should be 0-100 + this.rules.push({ + id: 'CANCELLATION_FEE_RANGE', + description: 'Cancellation fee percentage must be between 0 and 100', + severity: 'error', + validate: (rateSections: RateEntry[]) => { + const cancellationSection = rateSections.find( + s => s.rateType === RATE_TYPES.CANCELLATION_FEE + ); + if (!cancellationSection) return true; + + return cancellationSection.entries.every(entry => { + const percentage = Number(entry.cancellationFeePercentage); + return !percentage || (percentage >= 0 && percentage <= 100); + }); + }, + message: 'Cancellation fee percentage must be between 0 and 100', + }); + } + + validate(data: RateEntry[]) { + const errors: Array<{ ruleId: string; message: string; severity: string }> = []; + const warnings: Array<{ ruleId: string; message: string; severity: string }> = []; + + this.rules.forEach(rule => { + if (!rule.validate(data)) { + const issue = { + ruleId: rule.id, + message: rule.message, + severity: rule.severity, + }; + + if (rule.severity === 'error') { + errors.push(issue); + } else { + warnings.push(issue); + } + } + }); + + return { + isValid: errors.length === 0, + errors, + warnings, + }; + } + + // Check if matrix can transition to a new status + canTransition(fromStatus: string, toStatus: string, userRole: string): boolean { + const transitions: Record> = { + [MATRIX_STATUS.DRAFT]: [ + { to: MATRIX_STATUS.PENDING_APPROVAL, allowedRoles: ['Director'] }, + ], + [MATRIX_STATUS.PENDING_APPROVAL]: [ + { to: MATRIX_STATUS.ACTIVE, allowedRoles: ['Chief Executive'] }, + { to: MATRIX_STATUS.REJECTED, allowedRoles: ['Chief Executive'] }, + ], + }; + + const allowedTransitions = transitions[fromStatus] || []; + const transition = allowedTransitions.find(t => t.to === toStatus); + + return transition ? transition.allowedRoles.includes(userRole) : false; + } +} + +export const rateMatrixRulesEngine = new RateMatrixRulesEngine(); \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/constants/TANSTACK_QUEY_KEY.ts b/apps/edr-freight-web/backoffice/src/constants/TANSTACK_QUEY_KEY.ts index 42bfa33ff..34b1ed929 100644 --- a/apps/edr-freight-web/backoffice/src/constants/TANSTACK_QUEY_KEY.ts +++ b/apps/edr-freight-web/backoffice/src/constants/TANSTACK_QUEY_KEY.ts @@ -16,5 +16,21 @@ export const QUERY_KEYS = { ROOT: "customers", LIST: "list", BY_ID: "by-id" - } -} \ No newline at end of file + }, + // constants/queryKeys.ts - Add these keys + + rateMatrix: { + all: ['rate-matrices'] as const, + lists: () => [...QUERY_KEYS.rateMatrix.all, 'list'] as const, + list: (filters: Record) => [...QUERY_KEYS.rateMatrix.lists(), filters] as const, + details: () => [...QUERY_KEYS.rateMatrix.all, 'detail'] as const, + detail: (id: string) => [...QUERY_KEYS.rateMatrix.details(), id] as const, + }, + referenceData: { + all: ['reference-data'] as const, + ports: () => [...QUERY_KEYS.referenceData.all, 'ports'] as const, + cities: () => [...QUERY_KEYS.referenceData.all, 'cities'] as const, + containerTypes: () => [...QUERY_KEYS.referenceData.all, 'container-types'] as const, + currencies: () => [...QUERY_KEYS.referenceData.all, 'currencies'] as const, + }, +}; diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 739faf950..380ec7992 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -121,4 +121,18 @@ export const URL_CONSTANTS = { WEIGHT_LIMIT_RULE_BY_ID: (id: string | number) => `/weight-limit-rules/${id}`, }, + RATE_MATRIX: { + BASE: '/api/rate-matrices', + DRAFT: '/api/rate-matrices/draft', + SUBMIT: (id: string) => `/api/rate-matrices/${id}/submit`, + AUTHORIZE: (id: string) => `/api/rate-matrices/${id}/authorize`, + LIST: '/api/rate-matrices', + DETAIL: (id: string) => `/api/rate-matrices/${id}`, + }, + REFERENCE: { + PORTS: '/api/reference/ports', + CITIES: '/api/reference/cities', + CONTAINER_TYPES: '/api/reference/container-types', + CURRENCIES: '/api/reference/currencies', + }, }; diff --git a/apps/edr-freight-web/backoffice/src/constants/rateMatrixConstants.ts b/apps/edr-freight-web/backoffice/src/constants/rateMatrixConstants.ts new file mode 100644 index 000000000..ff8d3bb80 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/constants/rateMatrixConstants.ts @@ -0,0 +1,60 @@ +// constants/rateMatrixConstants.ts +export const RATE_TYPES = { + CONTAINER_IMPORT: 'container_import', + CONTAINER_EXPORT: 'container_export', + BULK_IMPORT: 'bulk_import', + BULK_EXPORT: 'bulk_export', + INTER_CITY_BULK: 'inter_city_bulk', + INTER_CITY_CONTAINER: 'inter_city_container', + FIRST_MILE: 'first_mile', + LAST_MILE: 'last_mile', + DEMURRAGE: 'demurrage', + LASHING: 'lashing', + DOUBLE_HANDLING: 'double_handling', + CONTAINER_WITH_RETURN: 'container_with_return', + CANCELLATION_FEE: 'cancellation_fee', +} as const; + +export const RATE_TYPE_LABELS = { + [RATE_TYPES.CONTAINER_IMPORT]: 'Container Import Rates', + [RATE_TYPES.CONTAINER_EXPORT]: 'Container Export Rates', + [RATE_TYPES.BULK_IMPORT]: 'Bulk Import Rates', + [RATE_TYPES.BULK_EXPORT]: 'Bulk Export Rates', + [RATE_TYPES.INTER_CITY_BULK]: 'Inter City Bulk Rates', + [RATE_TYPES.INTER_CITY_CONTAINER]: 'Inter City Container Rates', + [RATE_TYPES.FIRST_MILE]: 'First Mile Cost Rates', + [RATE_TYPES.LAST_MILE]: 'Last Mile Cost Rates', + [RATE_TYPES.DEMURRAGE]: 'Demurrage Cost Rates', + [RATE_TYPES.LASHING]: 'Lashing Cost Rates', + [RATE_TYPES.DOUBLE_HANDLING]: 'Double Handling Cost Rates', + [RATE_TYPES.CONTAINER_WITH_RETURN]: 'Container With Return Cost Rates', + [RATE_TYPES.CANCELLATION_FEE]: 'Cancellation Fee Cost Rates', +} as const; + +export const REQUIRED_RATE_TYPES = Object.values(RATE_TYPES); + +export const RATE_FIELDS_CONFIG = { + [RATE_TYPES.CONTAINER_IMPORT]: [ + { name: 'portOfLoading', label: 'Port of Loading', type: 'text', required: true }, + { name: 'portOfDischarge', label: 'Port of Discharge', type: 'text', required: true }, + { name: 'containerType', label: 'Container Type', type: 'select', required: true }, + { name: 'baseRate', label: 'Base Rate', type: 'number', required: true, min: 0, step: '0.01' }, + { name: 'baf', label: 'Bunker Adjustment Factor', type: 'number', required: false, min: 0 }, + { name: 'caf', label: 'Currency Adjustment Factor', type: 'number', required: false, min: 0 }, + ], + [RATE_TYPES.DEMURRAGE]: [ + { name: 'containerType', label: 'Container Type', type: 'select', required: true }, + { name: 'freeDays', label: 'Free Days', type: 'number', required: true, min: 0 }, + { name: 'ratePerDay', label: 'Rate per Day', type: 'number', required: true, min: 0 }, + { name: 'maximumDays', label: 'Maximum Days', type: 'number', required: false, min: 1 }, + ], + // ... define for all 13 rate types +} as const; + +export const MATRIX_STATUS = { + DRAFT: 'draft', + PENDING_APPROVAL: 'pending_approval', + ACTIVE: 'active', + REJECTED: 'rejected', + EXPIRED: 'expired', +} as const; \ No newline at end of file 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 new file mode 100644 index 000000000..b44b5f3d4 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixApproval.tsx @@ -0,0 +1,123 @@ +// 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 { MATRIX_STATUS } from '@/constants/rateMatrixConstants'; +import { toast } from 'sonner'; + +const API_URLS = { + RATE_MATRIX: { + LIST: '/api/rate-matrices', + AUTHORIZE: (id: string) => `/api/rate-matrices/${id}/authorize`, + }, +}; +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 new file mode 100644 index 000000000..fc6855dc3 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/admin/rateMatrix/RateMatrixRegistration.tsx @@ -0,0 +1,25 @@ +// pages/admin/rateMatrix/RateMatrixRegistration.tsx +import React from 'react'; +import { RateMatrixForm } from '@/components/baselineRatematrix/RateMatrixForm'; +import { useRateMatrixAuth } from '../../../auth/hooks/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/tsconfig.json b/apps/edr-freight-web/backoffice/tsconfig.json index 1ffef600d..04f44ca2b 100644 --- a/apps/edr-freight-web/backoffice/tsconfig.json +++ b/apps/edr-freight-web/backoffice/tsconfig.json @@ -2,6 +2,18 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { + "compilerOptions": { + "baseUrl": "src", + "paths": { + "@/*": ["*"], + + "@constants/*": ["constants/*"], + "@components/*": ["components/*"] + } + } +} + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9b737e64..db3a0db68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,9 @@ importers: recharts: specifier: ^3.8.1 version: 3.8.1(@types/react@18.3.29)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.6)(react@19.2.6)(redux@5.0.1) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) tailwind-merge: specifier: ^3.6.0 version: 3.6.0