diff --git a/package-lock.json b/package-lock.json index 630df0c..1d39e59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/jsonwebtoken": "^9.0.10", "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.545.0", "next": "15.5.4", "next-themes": "^0.4.6", "react": "19.1.0", @@ -4594,6 +4595,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "0.545.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", + "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", diff --git a/package.json b/package.json index 377c988..4bb0c71 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@types/jsonwebtoken": "^9.0.10", "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.545.0", "next": "15.5.4", "next-themes": "^0.4.6", "react": "19.1.0", diff --git a/src/app/(protected)/orders/page.tsx b/src/app/(protected)/orders/page.tsx index a404e3f..9d92149 100644 --- a/src/app/(protected)/orders/page.tsx +++ b/src/app/(protected)/orders/page.tsx @@ -1,8 +1,17 @@ -// File: src/app/(protected)/orders/page.tsx "use client"; import { useEffect, useMemo, useState, type ReactNode } from "react"; +import type { LucideIcon } from "lucide-react"; +import { + ShoppingBag, ShoppingCart, Music2, Globe, MessageSquare, + CreditCard, QrCode, HandCoins, Wallet, Landmark, + CheckCircle2, Hourglass, XCircle, RotateCcw, + Package, Cog, Truck, Ban, + ShieldCheck, ShieldAlert, ShieldX, + Factory, Building2, Store +} from "lucide-react"; +/* ======================== Types ======================== */ type Channel = "shopee" | "lazada" | "tiktok" | "d2c" | "chat"; type PaymentStatus = "paid" | "pending" | "failed" | "cod" | "refunded"; type FulfillmentStatus = @@ -104,6 +113,125 @@ type Column = { format?: (v: Order[ColumnKey], row: Order) => ReactNode; }; +/* ======================== Meta Mapping (Uppercase + Icon) ======================== */ +type Tone = "ok" | "warn" | "bad" | "neutral"; +const pill = (t: Tone) => + ({ + ok: "bg-emerald-50 text-emerald-700 border border-emerald-200", + warn: "bg-amber-50 text-amber-700 border border-amber-200", + bad: "bg-red-50 text-red-700 border border-red-200", + neutral: "bg-neutral-50 text-neutral-700 border border-neutral-200", + }[t]); + +const U = (s: string) => s.toUpperCase(); + +const IconLabel = ({ Icon, label, pillTone }: { Icon: LucideIcon; label: string; pillTone?: Tone }) => { + const content = ( + + + {label} + + ); + if (!pillTone) return content; + return ( + + + {label} + + ); +}; + +/* Channel */ +const CHANNEL_META = { + shopee: { label: U("Shopee"), Icon: ShoppingBag }, + lazada: { label: U("Lazada"), Icon: ShoppingCart }, + tiktok: { label: U("TikTok"), Icon: Music2 }, + d2c: { label: U("D2C"), Icon: Globe }, + chat: { label: U("Chat"), Icon: MessageSquare }, +} satisfies Record; + +const renderChannel = (c: Channel) => { + const { Icon, label } = CHANNEL_META[c]; + return ; +}; + +/* Payment Method */ +const PAYMENT_METHOD_META = { + card: { label: U("Card"), Icon: CreditCard }, + bank_qr: { label: U("Bank QR"), Icon: QrCode }, + cod: { label: U("COD"), Icon: HandCoins }, + wallet: { label: U("Wallet"), Icon: Wallet }, + transfer: { label: U("Transfer"), Icon: Landmark }, +} satisfies Record; + +const renderPaymentMethod = (m: Order["paymentMethod"]) => { + const { Icon, label } = PAYMENT_METHOD_META[m]; + return ; +}; + +/* Payment Status */ +const PAYMENT_STATUS_META = { + paid: { label: U("Paid"), Icon: CheckCircle2, tone: "ok" as Tone }, + pending: { label: U("Pending"), Icon: Hourglass, tone: "warn" as Tone }, + failed: { label: U("Failed"), Icon: XCircle, tone: "bad" as Tone }, + cod: { label: U("COD"), Icon: HandCoins, tone: "warn" as Tone }, + refunded: { label: U("Refunded"), Icon: RotateCcw, tone: "neutral" as Tone }, +} satisfies Record; + +const renderPaymentStatus = (s: PaymentStatus) => { + const { Icon, label, tone } = PAYMENT_STATUS_META[s]; + return ; +}; + +/* Fulfillment Status */ +const FULFILL_META = { + unfulfilled: { label: U("Unfulfilled"), Icon: Package, tone: "warn" as Tone }, + processing: { label: U("Processing"), Icon: Cog, tone: "warn" as Tone }, + shipped: { label: U("Shipped"), Icon: Truck, tone: "warn" as Tone }, + delivered: { label: U("Delivered"), Icon: CheckCircle2, tone: "ok" as Tone }, + returned: { label: U("Returned"), Icon: RotateCcw, tone: "bad" as Tone }, + cancelled: { label: U("Cancelled"), Icon: Ban, tone: "bad" as Tone }, +} satisfies Record; + +const renderFulfillment = (f: FulfillmentStatus) => { + const { Icon, label, tone } = FULFILL_META[f]; + return ; +}; + +/* Fraud Status (nullable) */ +const FRAUD_META: Record< + NonNullable, + { label: string; Icon: LucideIcon; tone: Tone } +> = { + clear: { label: U("Clear"), Icon: ShieldCheck, tone: "ok" }, + review: { label: U("Review"), Icon: ShieldAlert, tone: "warn" }, + hold: { label: U("Hold"), Icon: ShieldX, tone: "bad" }, +}; +const renderFraud = (s: Order["fraudCheckStatus"]) => + s ? ( + + ) : ( + N/A + ); + +/* Warehouse */ +const WAREHOUSE_META = { + "BKK-DC": { label: U("BKK-DC"), Icon: Factory }, + "CNX-HUB": { label: U("CNX-HUB"), Icon: Building2 }, + "HKT-MINI": { label: U("HKT-MINI"), Icon: Store }, +} satisfies Record; + +const renderWarehouse = (w: Order["warehouseCode"]) => { + const { Icon, label } = WAREHOUSE_META[w]; + return ; +}; + +/* For selects (native