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 07e212b76..af92d38fd 100644 --- a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx @@ -12,9 +12,11 @@ import { Text, TextInput, } from "@mantine/core"; +import { Badge as MantineBadge } from "@mantine/core"; import { CheckCircle2, CircleDollarSign, + LayoutGrid, Loader2, RotateCcw, Search, @@ -24,6 +26,7 @@ import { } from "lucide-react"; import Breadcrumbs from "@/components/ui/Breadcrumbs"; +import "@/components/overview/overview.css"; import { usePaymentList, usePaymentSummary } from "@/hooks/usePayments"; import type { PaymentMethod, @@ -39,11 +42,16 @@ import { } from "@edr/ui-common"; const STATUS_TABS = [ - { key: "all", label: "All", statuses: undefined as string | undefined }, - { key: "success", label: "Success", statuses: "success" }, - { key: "processing", label: "Processing", statuses: "processing,action-required" }, - { key: "failed", label: "Failed", statuses: "failed,canceled" }, - { key: "refunded", label: "Refunded", statuses: "refunded" }, + { key: "all", label: "All", statuses: undefined as string | undefined, icon: LayoutGrid }, + { key: "success", label: "Success", statuses: "success", icon: CheckCircle2 }, + { + key: "processing", + label: "Processing", + statuses: "processing,action-required", + icon: Loader2, + }, + { key: "failed", label: "Failed", statuses: "failed,canceled", icon: XCircle }, + { key: "refunded", label: "Refunded", statuses: "refunded", icon: RotateCcw }, ] as const; type StatusTabKey = (typeof STATUS_TABS)[number]["key"]; @@ -166,6 +174,20 @@ export default function PaymentsPage() { const val = (n?: number) => (summaryLoading ? "—" : (n ?? 0)); + const tabCounts: Record = { + all: + summary === undefined + ? undefined + : (summary.success ?? 0) + + (summary.processing ?? 0) + + (summary.failed ?? 0) + + (summary.refunded ?? 0), + success: summary?.success, + processing: summary?.processing, + failed: summary?.failed, + refunded: summary?.refunded, + }; + const columns: ColumnDef[] = [ { id: "order", @@ -274,13 +296,48 @@ export default function PaymentsPage() { setStatusTab((value as StatusTabKey) ?? "all"); setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); }} + variant="pills" + color="green" + keepMounted={false} + classNames={{ list: "ov-tablist", tab: "ov-tab" }} > - {STATUS_TABS.map((t) => ( - - {t.label} - - ))} + {STATUS_TABS.map((t) => { + const isActive = statusTab === t.key; + const count = tabCounts[t.key]; + const Icon = t.icon; + return ( + } + rightSection={ + count !== undefined ? ( + + {count} + + ) : undefined + } + > + {t.label} + + ); + })} diff --git a/apps/edr-freight-web/portal/src/components/auth/AuthShell.tsx b/apps/edr-freight-web/portal/src/components/auth/AuthShell.tsx new file mode 100644 index 000000000..96d55bdd0 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/auth/AuthShell.tsx @@ -0,0 +1,151 @@ +import type { ReactNode } from "react"; +import { ArrowUpRight, ChevronDown, Globe } from "lucide-react"; + +const LOGIN_IMAGE = "/assets/login.png"; +const EDR_LOGO = "/assets/logo.svg"; + +export const fieldClass = + "h-11 w-full rounded-xl border border-gray-200/90 bg-white px-4 text-sm text-gray-900 shadow-sm placeholder:text-gray-400 outline-none transition-all duration-200 hover:border-gray-300 focus:border-primary focus:bg-white focus:ring-4 focus:ring-primary/10"; + +export const primaryButtonClass = + "h-11 w-full rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow-[0_8px_20px_-6px_rgba(16,94,52,0.5)] transition-all duration-200 hover:bg-primary/90 hover:shadow-[0_10px_24px_-6px_rgba(16,94,52,0.55)] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-none"; + +const LeftPanelDecor = () => ( +
+ + {[0, 1, 2, 3, 4, 5].map((ring) => ( + + ))} + +
+
+); + +const RightPanelDecor = () => ( +
+
+
+ + + + + + + + +
+); + +export interface AuthShellProps { + children: ReactNode; + /** Tagline shown in the highlighted card over the left image panel. */ + tagline?: string; + taglineBody?: string; +} + +const LeftPanel = ({ tagline, taglineBody }: Pick) => ( +
+ Ethio Djibouti Railway +
+ + + + +
+
+
+
+ + {tagline ?? "Empower Your Freight Operations"} + +
+

+ {taglineBody ?? + "Sign in to manage shipments, track cargo, and run logistics operations on the Ethio Djibouti Railway freight platform."} +

+
+
+
+); + +const LanguageSelector = () => ( +
+ + Eng + +
+); + +const FormFooter = () => ( + +); + +export default function AuthShell({ children, tagline, taglineBody }: AuthShellProps) { + return ( + <> + + + + +
+
+ + +
+ + +
+ +
+ +
+
+
+ {children} +
+
+
+ + +
+
+
+ + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx index f8ba4ff45..9eb5a8d9b 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx @@ -1,39 +1,51 @@ -import { Alert, Box, Button, Group, PasswordInput, SegmentedControl, Stack, Text, TextInput } from "@mantine/core"; -import { ArrowRight, Mail, Phone } from "lucide-react"; -import { useState } from "react"; +import { type FormEvent, useState } from "react"; +import { ChevronDown, Eye, EyeOff, Mail, Smartphone } from "lucide-react"; import { useLocation, useNavigate } from "react-router-dom"; import useAuth from "@/hooks/useAuth"; -import AuthLayout from "@/components/auth/AuthLayout"; -import PhoneInput from "@/components/auth/PhoneInput"; +import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell"; + +const EDR_LOGO = "/assets/logo.svg"; type LoginMethod = "email" | "phone"; +const loginMethods: Array<{ + value: LoginMethod; + label: string; + icon: typeof Mail; + placeholder: string; +}> = [ + { value: "email", label: "Email", icon: Mail, placeholder: "name@company.com" }, + { value: "phone", label: "Phone", icon: Smartphone, placeholder: "09XXXXXXXX" }, +]; + export default function LoginPage() { const navigate = useNavigate(); const location = useLocation(); const { login } = useAuth(); const [method, setMethod] = useState("email"); const [identifier, setIdentifier] = useState(""); - const [countryCode, setCountryCode] = useState("+251"); - const [phoneNumber, setPhoneNumber] = useState(""); + const [countryCode] = useState("+251"); const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const currentMethod = loginMethods.find((item) => item.value === method)!; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); setError(null); setLoading(true); try { const loginId = method === "email" ? identifier - : `${countryCode}${phoneNumber.startsWith("0") ? phoneNumber.slice(1) : phoneNumber}`; + : `${countryCode}${identifier.startsWith("0") ? identifier.slice(1) : identifier}`; const result = await login({ email: loginId, password }); if (result.success) { - const from = (location.state as { from?: { pathname: string } } | null) - ?.from?.pathname; + const from = (location.state as { from?: { pathname: string } } | null)?.from + ?.pathname; navigate(from ?? "/portal", { replace: true }); } else { setError(result.error.message); @@ -46,154 +58,105 @@ export default function LoginPage() { }; return ( - - - - - - - + +
+
+ EDR Freight +
+ +
+

Welcome back - - - Enter your credentials to access your portal - - - +

+

+ Enter your credentials to access your freight portal. +

+
- - - setMethod(v as LoginMethod)} - fullWidth - radius="md" - data={[ - { - label: ( - - - Email - - ), - value: "email", - }, - { - label: ( - - - Phone - - ), - value: "phone", - }, - ]} - /> - - {method === "email" ? ( - setIdentifier(e.target.value)} - required - disabled={loading} - /> - ) : ( - ) => - setCountryCode(e.target.value), - }} - phone={{ - value: phoneNumber, - onChange: (e: React.ChangeEvent) => - setPhoneNumber(e.target.value), - }} - /> - )} - - - - - Password - - +
+
+ + {error ? ( +
{error} - - )} +
+ ) : null} - + - - Don't have an account?{" "} - - - + +

+
- + ); } diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx index f85d11750..6de471b6d 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx @@ -1,7 +1,6 @@ -import { Alert, Box, Button, Group, PasswordInput, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Check, ArrowRight, UserPlus, X } from "lucide-react"; import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ArrowRight, Check, Eye, EyeOff, X } from "lucide-react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; @@ -9,8 +8,9 @@ import { z } from "zod"; import { userType } from "@/enums/userType"; import useAuth from "@/hooks/useAuth"; import type { SignupPayload } from "@/types/auth"; -import AuthLayout from "@/components/auth/AuthLayout"; -import PhoneInput from "@/components/auth/PhoneInput"; +import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell"; + +const EDR_LOGO = "/assets/logo.svg"; const passwordRequirements = [ { label: "At least 8 characters", test: (v: string) => v.length >= 8 }, @@ -44,11 +44,16 @@ const userSchema = z type FormData = z.infer; +const errorText = (msg?: string) => + msg ?

{msg}

: null; + export default function SignupPage() { const navigate = useNavigate(); const { signup } = useAuth(); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); const { register, @@ -102,145 +107,182 @@ export default function SignupPage() { const passwordValue = watch("password") ?? ""; return ( - - - - - - - - Create Account - - +
+
+ EDR Freight +
+ +
+

+ Create account +

+

Register to access EDR Freight services. - - - +

+
- {error && ( - - {error} - - )} +
+
+
+ + + {errorText(errors.firstName?.en?.message)} +
+
+ + + {errorText(errors.lastName?.en?.message)} +
+
- - - - + + - - + {errorText(errors.email?.message)} +
- +
+ +
+ + +
+ {errorText(errors.countryCode?.message ?? errors.phone?.message)} +
- - - - - {passwordValue.length > 0 && ( - +
+ +
+ + +
+ {errorText(errors.password?.message)} + {passwordValue.length > 0 ? ( +
{passwordRequirements.map((req) => { const met = req.test(passwordValue); return ( - - + - {met ? : } - - + {met ? : } + + {req.label} - - + +
); })} - - )} - +
+ ) : null} +
- +
+ +
+ + +
+ {errorText(errors.confirmPassword?.message)} +
-
+ ) : null} + + + {loading ? "Creating account..." : "Create Account"} + {!loading ? : null} + - +

Already have an account?{" "} - - - + +

+ - + ); }