From 8264df133fc1e71647d371c44eff3d45d5d9658b Mon Sep 17 00:00:00 2001 From: marshal Date: Fri, 19 Jun 2026 00:42:27 +0300 Subject: [PATCH] Redesign payment verification UI with Mantine components and fix route direction logic - Refactor CheckPaymentPage and PaymentFailurePage to use Mantine UI components with modern card-based layouts - Add gradient headers, themed icons, and improved visual hierarchy for payment status states - Implement PaymentCard wrapper component for consistent styling across payment pages - Update route direction logic to trim country values and simplify Djibouti import/export detection - Replace shadcn/ui components with M --- ...4000000001-SeedWagonsWithYardAssignment.ts | 156 +++++++ .../pages/bookings/new-booking-form/schema.ts | 18 +- .../src/pages/payments/CheckPaymentPage.tsx | 407 ++++++++++++++---- .../src/pages/payments/PaymentFailurePage.tsx | 186 ++++++-- .../src/pages/payments/PaymentSuccessPage.tsx | 183 ++++++-- 5 files changed, 800 insertions(+), 150 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1784000000001-SeedWagonsWithYardAssignment.ts diff --git a/apps/edr-freight-api/src/migrations/1784000000001-SeedWagonsWithYardAssignment.ts b/apps/edr-freight-api/src/migrations/1784000000001-SeedWagonsWithYardAssignment.ts new file mode 100644 index 000000000..a615fe9a4 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1784000000001-SeedWagonsWithYardAssignment.ts @@ -0,0 +1,156 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Full wagon re-seed — runs in this order: + * + * 1. DELETE all existing wagons (hard delete, not soft). + * 2. UPSERT all 10 standard wagon types so they are guaranteed to exist. + * 3. INSERT 50 wagons per wagon type (500 total), distributed evenly across + * the 5 main operational yards (10 wagons per yard per type): + * + * KALITY — Kality Rail Terminal + * MOJO — Mojo Dry Port + * DIRE_DAWA — Dire Dawa Yard + * DJIB_PORT — Djibouti Port Terminal + * NAGAD — Nagad Terminal, Djibouti + * + * Wagon numbers follow the pattern -NNNN (e.g. NW5-0001 … NW5-0050). + * Yard IDs are fetched live from freight.yards so the migration is safe across + * all environments regardless of UUID values. + */ +export class SeedWagonsWithYardAssignment1784000000001 + implements MigrationInterface +{ + name = 'SeedWagonsWithYardAssignment1784000000001'; + + public async up(queryRunner: QueryRunner): Promise { + // ── STEP 1: Remove all wagons ────────────────────────────────────────── + await queryRunner.query(`DELETE FROM freight.wagons;`); + + // ── STEP 2: Ensure all 10 wagon types exist ──────────────────────────── + await queryRunner.query(` + INSERT INTO freight.wagon_types ( + code, + name, + capacity_tons, + length_meters, + max_wagons_per_train, + supported_load_types, + is_active, + tare_weight_tons + ) + VALUES + ('NW7', 'Double deck sedan wagon', 22, 26.066, NULL, ARRAY['vehicles', 'sedan'], true, 18.0), + ('NW5', 'Flat wagon (container)', 70, 14.000, 53, ARRAY['container', 'steel', 'machinery'], true, 22.0), + ('PW2', 'Box wagon', 70, 17.066, 18, ARRAY['general cargo', 'break bulk'], true, 20.0), + ('GW2', 'Tank wagon', 70, 12.228, 37, ARRAY['liquid', 'fuel'], true, 25.0), + ('CW4', 'Gondola covered wagon', 70, 13.976, 37, ARRAY['covered bulk cargo'], true, 22.0), + ('CW3', 'Gondola open wagon', 70, 13.976, NULL, ARRAY['open bulk cargo'], true, 20.0), + ('KW2', 'Hopper covered wagon', 69, 16.466, NULL, ARRAY['bulk grains'], true, 22.0), + ('KW3', 'Hopper open wagon', 70, 14.400, NULL, ARRAY['coal', 'bulk cargo'], true, 20.0), + ('NW6', 'Flat wagon (long cargo)', 70, 18.560, NULL, ARRAY['long cargo'], true, 22.0), + ('BW1', 'Refrigerated wagon', 38, 21.996, NULL, ARRAY['refrigerated cargo'], true, 24.0) + ON CONFLICT (code) DO UPDATE SET + name = EXCLUDED.name, + capacity_tons = EXCLUDED.capacity_tons, + length_meters = EXCLUDED.length_meters, + max_wagons_per_train = EXCLUDED.max_wagons_per_train, + supported_load_types = EXCLUDED.supported_load_types, + is_active = true, + tare_weight_tons = EXCLUDED.tare_weight_tons, + deleted_at = NULL, + updated_at = now(); + `); + + // ── STEP 3: Seed 50 wagons per type across 5 yards ──────────────────── + await queryRunner.query(` + DO $$ + DECLARE + wt RECORD; + yard_kality UUID; + yard_mojo UUID; + yard_dire_dawa UUID; + yard_djib_port UUID; + yard_nagad UUID; + yards UUID[]; + i INT; + yard_id UUID; + wagon_num TEXT; + v_tare NUMERIC; + v_payload NUMERIC; + BEGIN + -- Fetch yard IDs by code (safe across envs — UUIDs differ per DB) + SELECT id INTO yard_kality FROM freight.yards WHERE code = 'KALITY' LIMIT 1; + SELECT id INTO yard_mojo FROM freight.yards WHERE code = 'MOJO' LIMIT 1; + SELECT id INTO yard_dire_dawa FROM freight.yards WHERE code = 'DIRE_DAWA' LIMIT 1; + SELECT id INTO yard_djib_port FROM freight.yards WHERE code = 'DJIB_PORT' LIMIT 1; + SELECT id INTO yard_nagad FROM freight.yards WHERE code = 'NAGAD' LIMIT 1; + + IF yard_kality IS NULL OR yard_mojo IS NULL OR yard_dire_dawa IS NULL + OR yard_djib_port IS NULL OR yard_nagad IS NULL + THEN + RAISE EXCEPTION 'One or more operational yards not found. Run the yards seed first.'; + END IF; + + yards := ARRAY[ + yard_kality, + yard_mojo, + yard_dire_dawa, + yard_djib_port, + yard_nagad + ]; + + FOR wt IN + SELECT id, code, capacity_tons, tare_weight_tons + FROM freight.wagon_types + WHERE is_active = true + ORDER BY code + LOOP + v_tare := COALESCE(wt.tare_weight_tons, 20.0); + v_payload := COALESCE(wt.capacity_tons, 60.0); + + FOR i IN 1 .. 50 LOOP + wagon_num := wt.code || '-' || LPAD(i::TEXT, 4, '0'); + yard_id := yards[ ((i - 1) % 5) + 1 ]; -- round-robin: 1→K, 2→M, 3→D, 4→J, 5→N, 6→K … + + INSERT INTO freight.wagons ( + id, + wagon_number, + wagon_type_id, + tare_weight, + max_payload_weight, + status, + current_yard_id, + train_id, + sequence_number, + notes, + train_set_wagon_id, + current_train_schedule_id, + created_at, + updated_at + ) + VALUES ( + uuid_generate_v4(), + wagon_num, + wt.id, + v_tare, + v_payload, + 'Available', + yard_id, + NULL, NULL, NULL, NULL, NULL, + now(), now() + ) + ON CONFLICT (wagon_number) DO NOTHING; + END LOOP; + + RAISE NOTICE 'Seeded 50 wagons for type %.', wt.code; + END LOOP; + END $$; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Remove all seeded wagons (full wipe — mirrors what up() did) + await queryRunner.query(`DELETE FROM freight.wagons;`); + } +} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts index ea4b1214a..118db50c1 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts @@ -293,17 +293,17 @@ export function getRouteDirection( dest: Freight.BookingReferenceYard | null | undefined, ): Freight.ScheduleTradeDirection | null { if (!origin || !dest) return null; - if (origin.country === "Ethiopia" && dest.country === "Ethiopia") { - return "DOMESTIC"; - } - if (origin.country === "Ethiopia" && dest.country === "Djibouti") { - return "EXPORT"; - } - if (origin.country === "Djibouti" && dest.country === "Ethiopia") { + + const originCountry = origin.country?.trim(); + const destCountry = dest.country?.trim(); + + if (originCountry === "Djibouti") { return "IMPORT"; } - - return null; + if (destCountry === "Djibouti" && originCountry !== "Djibouti") { + return "EXPORT"; + } + return "DOMESTIC"; } export function calcWagons(containers: ContainerConfig[]) { diff --git a/apps/edr-freight-web/portal/src/pages/payments/CheckPaymentPage.tsx b/apps/edr-freight-web/portal/src/pages/payments/CheckPaymentPage.tsx index f61763f74..2aaa0793c 100644 --- a/apps/edr-freight-web/portal/src/pages/payments/CheckPaymentPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/payments/CheckPaymentPage.tsx @@ -1,8 +1,16 @@ import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; -import { CheckCircle2, LoaderCircle, XCircle } from "lucide-react"; -import { Button } from "@edr/ui-common"; +import { + Box, + Button, + Divider, + Loader, + Stack, + Text, + ThemeIcon, +} from "@mantine/core"; +import { AlertTriangle, CheckCircle2, FileSearch, FileText, Home, RotateCcw } from "lucide-react"; import { api } from "@/services/api"; function extractOrderId(): string | null { @@ -13,6 +21,35 @@ function extractOrderId(): string | null { return segments[segments.length - 1] ?? null; } +function PaymentCard({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} + export default function CheckPaymentPage() { const navigate = useNavigate(); const orderId = useMemo(() => extractOrderId(), []); @@ -29,105 +66,307 @@ export default function CheckPaymentPage() { if (!orderId) { return ( -
-
-
- -

- No payment reference found -

- -
-
-
+ + + + + + + No payment reference + + + We could not find a payment order to verify. + + + + + + ); } - return ( -
-
- {isLoading && ( -
- -

- Checking payment status… -

-
- )} + if (isLoading) { + return ( + + + + + Verifying your payment… + + + Please wait, this usually takes a few seconds. + + + + ); + } - {isSuccess && ( -
-
- -
-

- Payment was successful! -

-

- Your booking has been confirmed and payment is complete. -

+ if (isSuccess) { + return ( + + + + + + + Payment verified + + + Your booking is confirmed and payment is complete. + + + + + + + EDR staff will assign a train and you will be notified of any updates. + + + + -
- )} - - {!isLoading && data && !isSuccess && ( -
-
- -
-

- Payment status: {data.status} -

-

- Please try again or contact support if the issue persists. -

-
- )} + + + Questions?{" "} + + support@edr.et + + + + + ); + } - {isError && ( -
-
- -
-

- Something went wrong -

-

+ if (isError) { + return ( + + + + + + + Verification failed + + + We could not verify your payment status. + + + + + + {error instanceof Error ? error.message - : "Failed to check payment status."} -

+ : "An unexpected error occurred. Please try again or contact support."} + + + + -
- )} -
-
+ + + + Need help?{" "} + + support@edr.et + + + + + ); + } + + // Non-success status (e.g. PAY_FAIL, PENDING, etc.) + return ( + + + + + + + Payment incomplete + + + Status:{" "} + + {data?.status ?? "Unknown"} + + + + + + + + Your payment did not complete successfully. Nothing has been charged. + You can retry from your booking page. + + + + + + + + + Need help?{" "} + + support@edr.et + + + + ); } diff --git a/apps/edr-freight-web/portal/src/pages/payments/PaymentFailurePage.tsx b/apps/edr-freight-web/portal/src/pages/payments/PaymentFailurePage.tsx index 6b577ac06..3d59c61cb 100644 --- a/apps/edr-freight-web/portal/src/pages/payments/PaymentFailurePage.tsx +++ b/apps/edr-freight-web/portal/src/pages/payments/PaymentFailurePage.tsx @@ -1,43 +1,171 @@ -import { XCircle } from "lucide-react"; +import { + Box, + Button, + Divider, + Group, + Stack, + Text, + ThemeIcon, +} from "@mantine/core"; +import { AlertTriangle, ArrowLeft, Home, RotateCcw } from "lucide-react"; import { useNavigate } from "react-router-dom"; -import { Button } from "@edr/ui-common"; -/** - * Public page the payment provider redirects the browser to after a failed or - * cancelled payment (PAYMENT_FAILURE_URL). Generic — it explains nothing was - * charged and sends the customer back to their bookings to retry from "Pay now". - */ export default function PaymentFailurePage() { const navigate = useNavigate(); return ( -
-
-
-
- -
-

- Payment was not completed -

-

- Your payment didn't go through and you haven't been charged. You can - try again from your booking using "Pay now". -

-
- -
-
-
-
+ + + + Need help?{" "} + + support@edr.et + + + + + ); } diff --git a/apps/edr-freight-web/portal/src/pages/payments/PaymentSuccessPage.tsx b/apps/edr-freight-web/portal/src/pages/payments/PaymentSuccessPage.tsx index 4b2d766e7..de9a20c0e 100644 --- a/apps/edr-freight-web/portal/src/pages/payments/PaymentSuccessPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/payments/PaymentSuccessPage.tsx @@ -1,43 +1,170 @@ -import { CheckCircle2 } from "lucide-react"; +import { + Box, + Button, + Divider, + Group, + Stack, + Text, + ThemeIcon, +} from "@mantine/core"; +import { CheckCircle2, FileText, Home } from "lucide-react"; import { useNavigate } from "react-router-dom"; -import { Button } from "@edr/ui-common"; -/** - * Public page the payment provider redirects the browser to after a successful - * payment (PAYMENT_RETURN_URL). Generic — it confirms success and points the - * customer back to their bookings, where the booking reflects the paid state. - */ export default function PaymentSuccessPage() { const navigate = useNavigate(); return ( -
-
-
-
- -
-

+ + + {/* Green header stripe */} + + + + + Payment successful -

-

- Thank you — your payment has been received. Your booking will be - updated shortly and is now confirmed for scheduling. -

-
- -
-
-
-
+ + + + Questions? Contact{" "} + + support@edr.et + + + + + ); }