mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 20:38:17 +00:00
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
This commit is contained in:
@@ -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 <TYPE_CODE>-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<void> {
|
||||||
|
// ── 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<void> {
|
||||||
|
// Remove all seeded wagons (full wipe — mirrors what up() did)
|
||||||
|
await queryRunner.query(`DELETE FROM freight.wagons;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -293,17 +293,17 @@ export function getRouteDirection(
|
|||||||
dest: Freight.BookingReferenceYard | null | undefined,
|
dest: Freight.BookingReferenceYard | null | undefined,
|
||||||
): Freight.ScheduleTradeDirection | null {
|
): Freight.ScheduleTradeDirection | null {
|
||||||
if (!origin || !dest) return null;
|
if (!origin || !dest) return null;
|
||||||
if (origin.country === "Ethiopia" && dest.country === "Ethiopia") {
|
|
||||||
return "DOMESTIC";
|
const originCountry = origin.country?.trim();
|
||||||
}
|
const destCountry = dest.country?.trim();
|
||||||
if (origin.country === "Ethiopia" && dest.country === "Djibouti") {
|
|
||||||
return "EXPORT";
|
if (originCountry === "Djibouti") {
|
||||||
}
|
|
||||||
if (origin.country === "Djibouti" && dest.country === "Ethiopia") {
|
|
||||||
return "IMPORT";
|
return "IMPORT";
|
||||||
}
|
}
|
||||||
|
if (destCountry === "Djibouti" && originCountry !== "Djibouti") {
|
||||||
return null;
|
return "EXPORT";
|
||||||
|
}
|
||||||
|
return "DOMESTIC";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calcWagons(containers: ContainerConfig[]) {
|
export function calcWagons(containers: ContainerConfig[]) {
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { CheckCircle2, LoaderCircle, XCircle } from "lucide-react";
|
import {
|
||||||
import { Button } from "@edr/ui-common";
|
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";
|
import { api } from "@/services/api";
|
||||||
|
|
||||||
function extractOrderId(): string | null {
|
function extractOrderId(): string | null {
|
||||||
@@ -13,6 +21,35 @@ function extractOrderId(): string | null {
|
|||||||
return segments[segments.length - 1] ?? null;
|
return segments[segments.length - 1] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PaymentCard({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
minHeight: "100dvh",
|
||||||
|
background: "#f8fafc",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 460,
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: 24,
|
||||||
|
border: "1.5px solid #e5e7eb",
|
||||||
|
boxShadow: "0 4px 24px 0 rgba(0,0,0,0.07)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function CheckPaymentPage() {
|
export default function CheckPaymentPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const orderId = useMemo(() => extractOrderId(), []);
|
const orderId = useMemo(() => extractOrderId(), []);
|
||||||
@@ -29,105 +66,307 @@ export default function CheckPaymentPage() {
|
|||||||
|
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
<PaymentCard>
|
||||||
<div className="w-full max-w-md rounded-2xl border border-border bg-card p-8 text-center shadow-sm">
|
<Box
|
||||||
<div className="flex flex-col items-center gap-4">
|
style={{
|
||||||
<XCircle className="size-10 text-destructive" />
|
background: "linear-gradient(135deg, #f97316 0%, #fb923c 100%)",
|
||||||
<p className="text-lg font-bold text-foreground">
|
padding: "40px 32px 32px",
|
||||||
No payment reference found
|
textAlign: "center",
|
||||||
</p>
|
}}
|
||||||
<Button
|
>
|
||||||
type="button"
|
<ThemeIcon
|
||||||
variant="outline"
|
size={80}
|
||||||
onClick={() => navigate("/bookings")}
|
radius="xl"
|
||||||
>
|
style={{
|
||||||
Back to My Bookings
|
background: "rgba(255,255,255,0.18)",
|
||||||
</Button>
|
border: "2px solid rgba(255,255,255,0.3)",
|
||||||
</div>
|
margin: "0 auto 20px",
|
||||||
</div>
|
display: "flex",
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
<FileSearch size={42} color="#fff" />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={800} fz={24} c="#fff" lh={1.2}>
|
||||||
|
No payment reference
|
||||||
|
</Text>
|
||||||
|
<Text fz={14} c="rgba(255,255,255,0.82)" mt={8}>
|
||||||
|
We could not find a payment order to verify.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Stack gap={10} p={32}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
color="orange"
|
||||||
|
onClick={() => navigate("/bookings")}
|
||||||
|
styles={{ root: { height: 48, fontWeight: 700 } }}
|
||||||
|
>
|
||||||
|
Back to my bookings
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</PaymentCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (isLoading) {
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
return (
|
||||||
<div className="w-full max-w-md rounded-2xl border border-border bg-card p-8 text-center shadow-sm">
|
<PaymentCard>
|
||||||
{isLoading && (
|
<Box style={{ padding: "64px 32px", textAlign: "center" }}>
|
||||||
<div className="flex flex-col items-center gap-4">
|
<Loader size={48} color="edr-green" type="dots" mx="auto" mb={24} />
|
||||||
<LoaderCircle className="size-10 animate-spin text-primary" />
|
<Text fw={700} fz={18} c="#10202F">
|
||||||
<p className="text-lg font-semibold text-foreground">
|
Verifying your payment…
|
||||||
Checking payment status…
|
</Text>
|
||||||
</p>
|
<Text fz={14} c="dimmed" mt={8}>
|
||||||
</div>
|
Please wait, this usually takes a few seconds.
|
||||||
)}
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</PaymentCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{isSuccess && (
|
if (isSuccess) {
|
||||||
<div className="flex flex-col items-center gap-4">
|
return (
|
||||||
<div className="flex size-14 items-center justify-center rounded-full bg-primary/10">
|
<PaymentCard>
|
||||||
<CheckCircle2 className="size-8 text-primary" />
|
<Box
|
||||||
</div>
|
style={{
|
||||||
<p className="text-lg font-bold text-foreground">
|
background: "linear-gradient(135deg, #059669 0%, #0ea371 100%)",
|
||||||
Payment was successful!
|
padding: "40px 32px 32px",
|
||||||
</p>
|
textAlign: "center",
|
||||||
<p className="text-sm text-muted-foreground">
|
}}
|
||||||
Your booking has been confirmed and payment is complete.
|
>
|
||||||
</p>
|
<ThemeIcon
|
||||||
|
size={80}
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.2)",
|
||||||
|
border: "2px solid rgba(255,255,255,0.35)",
|
||||||
|
margin: "0 auto 20px",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircle2 size={42} color="#fff" strokeWidth={2} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={800} fz={24} c="#fff" lh={1.2}>
|
||||||
|
Payment verified
|
||||||
|
</Text>
|
||||||
|
<Text fz={14} c="rgba(255,255,255,0.82)" mt={8} lh={1.5}>
|
||||||
|
Your booking is confirmed and payment is complete.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack gap={0} p={32}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "#f0fdf4",
|
||||||
|
border: "1px solid #bbf7d0",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: "14px 18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fz={13.5} c="#14532d" lh={1.5} ta="center">
|
||||||
|
EDR staff will assign a train and you will be notified of any updates.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Divider my={24} color="#e5e7eb" />
|
||||||
|
<Stack gap={10}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
color="edr-green"
|
||||||
|
leftSection={<FileText size={17} />}
|
||||||
onClick={() => navigate("/bookings")}
|
onClick={() => navigate("/bookings")}
|
||||||
className="mt-2"
|
styles={{ root: { height: 48, fontWeight: 700, fontSize: 15 } }}
|
||||||
>
|
>
|
||||||
Go to My Bookings
|
Go to my bookings
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && data && !isSuccess && (
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
|
||||||
<div className="flex size-14 items-center justify-center rounded-full bg-destructive/10">
|
|
||||||
<XCircle className="size-8 text-destructive" />
|
|
||||||
</div>
|
|
||||||
<p className="text-lg font-bold text-foreground">
|
|
||||||
Payment status: {data.status}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Please try again or contact support if the issue persists.
|
|
||||||
</p>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
fullWidth
|
||||||
variant="outline"
|
size="md"
|
||||||
onClick={() => navigate("/bookings")}
|
radius={12}
|
||||||
className="mt-2"
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
leftSection={<Home size={17} />}
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
styles={{ root: { height: 44, fontWeight: 600, fontSize: 14 } }}
|
||||||
>
|
>
|
||||||
Back to My Bookings
|
Back to home
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Stack>
|
||||||
)}
|
<Text fz={12} c="dimmed" ta="center" mt={20}>
|
||||||
|
Questions?{" "}
|
||||||
|
<Text span c="edr-green" fw={600}>
|
||||||
|
support@edr.et
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</PaymentCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{isError && (
|
if (isError) {
|
||||||
<div className="flex flex-col items-center gap-4">
|
return (
|
||||||
<div className="flex size-14 items-center justify-center rounded-full bg-destructive/10">
|
<PaymentCard>
|
||||||
<XCircle className="size-8 text-destructive" />
|
<Box
|
||||||
</div>
|
style={{
|
||||||
<p className="text-lg font-bold text-foreground">
|
background: "linear-gradient(135deg, #dc2626 0%, #ef4444 100%)",
|
||||||
Something went wrong
|
padding: "40px 32px 32px",
|
||||||
</p>
|
textAlign: "center",
|
||||||
<p className="text-sm text-muted-foreground">
|
}}
|
||||||
|
>
|
||||||
|
<ThemeIcon
|
||||||
|
size={80}
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.18)",
|
||||||
|
border: "2px solid rgba(255,255,255,0.3)",
|
||||||
|
margin: "0 auto 20px",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertTriangle size={42} color="#fff" strokeWidth={2} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={800} fz={24} c="#fff" lh={1.2}>
|
||||||
|
Verification failed
|
||||||
|
</Text>
|
||||||
|
<Text fz={14} c="rgba(255,255,255,0.82)" mt={8} lh={1.5}>
|
||||||
|
We could not verify your payment status.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack gap={0} p={32}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "#fff7f7",
|
||||||
|
border: "1px solid #fecaca",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: "14px 18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fz={13.5} c="#7f1d1d" ta="center" lh={1.5}>
|
||||||
{error instanceof Error
|
{error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: "Failed to check payment status."}
|
: "An unexpected error occurred. Please try again or contact support."}
|
||||||
</p>
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Divider my={24} color="#e5e7eb" />
|
||||||
|
<Stack gap={10}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
fullWidth
|
||||||
variant="outline"
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
color="red"
|
||||||
|
leftSection={<RotateCcw size={17} />}
|
||||||
onClick={() => navigate("/bookings")}
|
onClick={() => navigate("/bookings")}
|
||||||
className="mt-2"
|
styles={{ root: { height: 48, fontWeight: 700, fontSize: 15 } }}
|
||||||
>
|
>
|
||||||
Back to My Bookings
|
Back to my bookings
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
)}
|
fullWidth
|
||||||
</div>
|
size="md"
|
||||||
</div>
|
radius={12}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
leftSection={<Home size={17} />}
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
styles={{ root: { height: 44, fontWeight: 600, fontSize: 14 } }}
|
||||||
|
>
|
||||||
|
Back to home
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Text fz={12} c="dimmed" ta="center" mt={20}>
|
||||||
|
Need help?{" "}
|
||||||
|
<Text span c="red.6" fw={600}>
|
||||||
|
support@edr.et
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</PaymentCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-success status (e.g. PAY_FAIL, PENDING, etc.)
|
||||||
|
return (
|
||||||
|
<PaymentCard>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #d97706 0%, #f59e0b 100%)",
|
||||||
|
padding: "40px 32px 32px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeIcon
|
||||||
|
size={80}
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.18)",
|
||||||
|
border: "2px solid rgba(255,255,255,0.3)",
|
||||||
|
margin: "0 auto 20px",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertTriangle size={42} color="#fff" strokeWidth={2} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={800} fz={24} c="#fff" lh={1.2}>
|
||||||
|
Payment incomplete
|
||||||
|
</Text>
|
||||||
|
<Text fz={14} c="rgba(255,255,255,0.82)" mt={8} lh={1.5}>
|
||||||
|
Status:{" "}
|
||||||
|
<Text span fw={700}>
|
||||||
|
{data?.status ?? "Unknown"}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack gap={0} p={32}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "#fffbeb",
|
||||||
|
border: "1px solid #fde68a",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: "14px 18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fz={13.5} c="#78350f" ta="center" lh={1.5}>
|
||||||
|
Your payment did not complete successfully. Nothing has been charged.
|
||||||
|
You can retry from your booking page.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Divider my={24} color="#e5e7eb" />
|
||||||
|
<Stack gap={10}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
color="orange"
|
||||||
|
leftSection={<RotateCcw size={17} />}
|
||||||
|
onClick={() => navigate("/bookings")}
|
||||||
|
styles={{ root: { height: 48, fontWeight: 700, fontSize: 15 } }}
|
||||||
|
>
|
||||||
|
Back to my bookings — retry payment
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
leftSection={<Home size={17} />}
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
styles={{ root: { height: 44, fontWeight: 600, fontSize: 14 } }}
|
||||||
|
>
|
||||||
|
Back to home
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Text fz={12} c="dimmed" ta="center" mt={20}>
|
||||||
|
Need help?{" "}
|
||||||
|
<Text span c="orange.7" fw={600}>
|
||||||
|
support@edr.et
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</PaymentCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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() {
|
export default function PaymentFailurePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
<Box
|
||||||
<div className="w-full max-w-md rounded-2xl border border-border bg-card p-8 text-center shadow-sm">
|
style={{
|
||||||
<div className="flex flex-col items-center gap-4">
|
minHeight: "100dvh",
|
||||||
<div className="flex size-16 items-center justify-center rounded-full bg-destructive/10">
|
background: "linear-gradient(135deg, #fff7f7 0%, #f8fafc 60%, #fef2f2 100%)",
|
||||||
<XCircle className="size-9 text-destructive" />
|
display: "flex",
|
||||||
</div>
|
alignItems: "center",
|
||||||
<p className="text-xl font-bold text-foreground">
|
justifyContent: "center",
|
||||||
Payment was not completed
|
padding: "24px",
|
||||||
</p>
|
}}
|
||||||
<p className="text-sm text-muted-foreground">
|
>
|
||||||
Your payment didn't go through and you haven't been charged. You can
|
<Box
|
||||||
try again from your booking using "Pay now".
|
style={{
|
||||||
</p>
|
width: "100%",
|
||||||
<div className="mt-2 flex w-full flex-col gap-2">
|
maxWidth: 460,
|
||||||
<Button type="button" onClick={() => navigate("/bookings")}>
|
background: "#fff",
|
||||||
Back to My Bookings
|
borderRadius: 24,
|
||||||
|
border: "1.5px solid #fecaca",
|
||||||
|
boxShadow:
|
||||||
|
"0 4px 24px 0 rgba(220,38,38,0.07), 0 1px 4px 0 rgba(0,0,0,0.04)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Red header stripe */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #dc2626 0%, #ef4444 100%)",
|
||||||
|
padding: "40px 32px 32px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeIcon
|
||||||
|
size={80}
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.18)",
|
||||||
|
border: "2px solid rgba(255,255,255,0.3)",
|
||||||
|
margin: "0 auto 20px",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertTriangle size={42} color="#fff" strokeWidth={2} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={800} fz={24} c="#fff" lh={1.2}>
|
||||||
|
Payment not completed
|
||||||
|
</Text>
|
||||||
|
<Text fz={14} c="rgba(255,255,255,0.82)" mt={8} lh={1.5}>
|
||||||
|
Nothing was charged — your booking is still active.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<Stack gap={0} p={32}>
|
||||||
|
<Stack gap={16}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "#fff7f7",
|
||||||
|
border: "1px solid #fecaca",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: "16px 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap={10}>
|
||||||
|
<Group gap={10} wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#ef4444",
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text fz={13.5} c="#7f1d1d" lh={1.5}>
|
||||||
|
Your payment was declined or cancelled. No charge was made.
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap={10} wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#ef4444",
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text fz={13.5} c="#7f1d1d" lh={1.5}>
|
||||||
|
You can retry using the <strong>Pay now</strong> button on
|
||||||
|
your booking page.
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap={10} wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#ef4444",
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text fz={13.5} c="#7f1d1d" lh={1.5}>
|
||||||
|
Contact support if the problem persists.
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider my={24} color="#e5e7eb" />
|
||||||
|
|
||||||
|
<Stack gap={10}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
color="red"
|
||||||
|
leftSection={<RotateCcw size={17} />}
|
||||||
|
onClick={() => navigate("/bookings")}
|
||||||
|
styles={{
|
||||||
|
root: { height: 48, fontWeight: 700, fontSize: 15 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back to my bookings — retry payment
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
fullWidth
|
||||||
variant="outline"
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
leftSection={<Home size={17} />}
|
||||||
onClick={() => navigate("/")}
|
onClick={() => navigate("/")}
|
||||||
|
styles={{
|
||||||
|
root: { height: 44, fontWeight: 600, fontSize: 14 },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Back to home
|
Back to home
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Stack>
|
||||||
</div>
|
|
||||||
</div>
|
<Text fz={12} c="dimmed" ta="center" mt={20} lh={1.5}>
|
||||||
</div>
|
Need help?{" "}
|
||||||
|
<Text span c="red.6" fw={600}>
|
||||||
|
support@edr.et
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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() {
|
export default function PaymentSuccessPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
<Box
|
||||||
<div className="w-full max-w-md rounded-2xl border border-border bg-card p-8 text-center shadow-sm">
|
style={{
|
||||||
<div className="flex flex-col items-center gap-4">
|
minHeight: "100dvh",
|
||||||
<div className="flex size-16 items-center justify-center rounded-full bg-primary/10">
|
background: "linear-gradient(135deg, #f0fdf4 0%, #f8fafc 60%, #ecfdf5 100%)",
|
||||||
<CheckCircle2 className="size-9 text-primary" />
|
display: "flex",
|
||||||
</div>
|
alignItems: "center",
|
||||||
<p className="text-xl font-bold text-foreground">
|
justifyContent: "center",
|
||||||
|
padding: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 460,
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: 24,
|
||||||
|
border: "1.5px solid #d1fae5",
|
||||||
|
boxShadow:
|
||||||
|
"0 4px 24px 0 rgba(10,111,77,0.08), 0 1px 4px 0 rgba(0,0,0,0.04)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Green header stripe */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #059669 0%, #0ea371 100%)",
|
||||||
|
padding: "40px 32px 32px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeIcon
|
||||||
|
size={80}
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
background: "rgba(255,255,255,0.2)",
|
||||||
|
border: "2px solid rgba(255,255,255,0.35)",
|
||||||
|
margin: "0 auto 20px",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircle2 size={42} color="#fff" strokeWidth={2} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={800} fz={24} c="#fff" lh={1.2}>
|
||||||
Payment successful
|
Payment successful
|
||||||
</p>
|
</Text>
|
||||||
<p className="text-sm text-muted-foreground">
|
<Text fz={14} c="rgba(255,255,255,0.82)" mt={8} lh={1.5}>
|
||||||
Thank you — your payment has been received. Your booking will be
|
Your payment has been received and confirmed.
|
||||||
updated shortly and is now confirmed for scheduling.
|
</Text>
|
||||||
</p>
|
</Box>
|
||||||
<div className="mt-2 flex w-full flex-col gap-2">
|
|
||||||
<Button type="button" onClick={() => navigate("/bookings")}>
|
{/* Body */}
|
||||||
Go to My Bookings
|
<Stack gap={0} p={32}>
|
||||||
|
<Stack gap={16}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: "#f0fdf4",
|
||||||
|
border: "1px solid #bbf7d0",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: "16px 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap={10}>
|
||||||
|
<Group gap={10} wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#16a34a",
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text fz={13.5} c="#14532d" lh={1.5}>
|
||||||
|
Your booking is now confirmed for scheduling.
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap={10} wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#16a34a",
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text fz={13.5} c="#14532d" lh={1.5}>
|
||||||
|
A receipt will be sent to your registered email.
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group gap={10} wrap="nowrap">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#16a34a",
|
||||||
|
flexShrink: 0,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text fz={13.5} c="#14532d" lh={1.5}>
|
||||||
|
EDR staff will process your booking and assign a train.
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider my={24} color="#e5e7eb" />
|
||||||
|
|
||||||
|
<Stack gap={10}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
color="edr-green"
|
||||||
|
leftSection={<FileText size={17} />}
|
||||||
|
onClick={() => navigate("/bookings")}
|
||||||
|
styles={{
|
||||||
|
root: { height: 48, fontWeight: 700, fontSize: 15 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go to my bookings
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
fullWidth
|
||||||
variant="outline"
|
size="md"
|
||||||
|
radius={12}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
leftSection={<Home size={17} />}
|
||||||
onClick={() => navigate("/")}
|
onClick={() => navigate("/")}
|
||||||
|
styles={{
|
||||||
|
root: { height: 44, fontWeight: 600, fontSize: 14 },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Back to home
|
Back to home
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Stack>
|
||||||
</div>
|
|
||||||
</div>
|
<Text fz={12} c="dimmed" ta="center" mt={20} lh={1.5}>
|
||||||
</div>
|
Questions? Contact{" "}
|
||||||
|
<Text span c="edr-green" fw={600}>
|
||||||
|
support@edr.et
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user