Files
smart-electronics-frontend/features/orders/components/OrderPage.tsx
2026-02-07 16:06:33 +05:00

530 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useCallback, useMemo } from "react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
ChevronDown,
ChevronUp,
Package,
Calendar,
MapPin,
CreditCard,
ShoppingBag,
} from "lucide-react";
import { toast } from "sonner";
import { useOrders, useCancelOrder } from "@/lib/hooks";
import { useTranslations } from "next-intl";
import type { Order } from "@/lib/types/api";
import EmptyOrders from "./EmptyOrders";
import ErrorPage from "@/components/ErrorPage";
interface OrdersPageClientProps {
locale: string;
}
export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
const [orderToCancel, setOrderToCancel] = useState<Order | null>(null);
const [expandedOrders, setExpandedOrders] = useState<Set<number>>(new Set());
const t = useTranslations();
const { data: orders, isLoading, isError } = useOrders();
const { mutate: cancelOrder, isPending: isCancellingOrder } =
useCancelOrder();
const toggleOrderExpand = useCallback((orderId: number) => {
setExpandedOrders((prev) => {
const newSet = new Set(prev);
if (newSet.has(orderId)) {
newSet.delete(orderId);
} else {
newSet.add(orderId);
}
return newSet;
});
}, []);
const handleCancelOrder = useCallback((order: Order) => {
setOrderToCancel(order);
setIsCancelDialogOpen(true);
}, []);
const confirmCancelOrder = useCallback(() => {
if (!orderToCancel) return;
cancelOrder(orderToCancel.id, {
onSuccess: () => {
toast.success(t("order_cancelled"));
setIsCancelDialogOpen(false);
setOrderToCancel(null);
},
onError: (error: any) => {
toast.error(error.message || t("cancel_order_failed"));
},
});
}, [orderToCancel, cancelOrder, toast, t]);
const getStatusBadge = useCallback((status: string) => {
const lowerStatus = status.toLowerCase();
if (
lowerStatus.includes("ожидается") ||
lowerStatus.includes("pending") ||
lowerStatus.includes("garaşlama")
) {
return (
<Badge
variant="outline"
className="bg-amber-50 text-amber-700 border-amber-300 font-semibold"
>
{status}
</Badge>
);
}
if (
lowerStatus.includes("обработка") ||
lowerStatus.includes("processing") ||
lowerStatus.includes("işlenýär")
) {
return (
<Badge
variant="secondary"
className="bg-gray-100 text-gray-900 font-semibold"
>
{status}
</Badge>
);
}
if (
lowerStatus.includes("отправлен") ||
lowerStatus.includes("shipped") ||
lowerStatus.includes("iberildi")
) {
return (
<Badge className="bg-indigo-500 hover:bg-indigo-500 font-semibold">
{status}
</Badge>
);
}
if (
lowerStatus.includes("доставлен") ||
lowerStatus.includes("delivered") ||
lowerStatus.includes("eltildi")
) {
return (
<Badge className="bg-emerald-600 hover:bg-emerald-600 font-semibold">
{status}
</Badge>
);
}
if (
lowerStatus.includes("отменен") ||
lowerStatus.includes("cancelled") ||
lowerStatus.includes("ýatyryldy")
) {
return (
<Badge className="bg-red-600 hover:bg-red-600 font-semibold">
{status}
</Badge>
);
}
return <Badge className="font-semibold">{status}</Badge>;
}, []);
const isActiveOrder = useCallback((status: string) => {
const lower = status.toLowerCase();
return (
lower.includes("ожидается") ||
lower.includes("обработка") ||
lower.includes("отправлен") ||
lower.includes("pending") ||
lower.includes("processing") ||
lower.includes("shipped") ||
lower.includes("garaşylýar") ||
lower.includes("işlenýär") ||
lower.includes("iberildi")
);
}, []);
const activeOrders = useMemo(
() => orders?.filter((o) => isActiveOrder(o.status)) || [],
[orders, isActiveOrder],
);
const completedOrders = useMemo(
() => orders?.filter((o) => !isActiveOrder(o.status)) || [],
[orders, isActiveOrder],
);
const calculateTotal = useCallback((order: Order) => {
return order.orderItems.reduce((sum, item) => {
return sum + parseFloat(item.unit_price_amount) * item.quantity;
}, 0);
}, []);
if (isLoading) {
return (
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6 text-gray-900">
{t("my_orders")}
</h1>
{/* Tabs Skeleton */}
<div className="mb-4 md:mb-6">
<div className="flex gap-2 mb-4">
<Skeleton className="h-10 w-32 rounded-xl" />
<Skeleton className="h-10 w-32 rounded-xl" />
</div>
</div>
{/* Order Cards Skeleton */}
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<Card
key={i}
className="overflow-hidden py-2 md:py-4 lg:py-6 rounded-2xl border border-gray-200"
>
<div className="p-2 md:p-4 mx-2 md:mx-4 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<Skeleton className="h-5 w-5 rounded" />
<div className="space-y-2">
<Skeleton className="h-5 w-32 rounded-lg" />
<Skeleton className="h-4 w-24 rounded-lg" />
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col md:flex-row gap-2 items-end">
<Skeleton className="h-6 w-20 rounded-full" />
<Skeleton className="h-6 w-24 rounded-lg" />
</div>
<Skeleton className="h-5 w-5 rounded" />
</div>
</div>
</div>
</Card>
))}
</div>
</div>
);
}
if (isError) {
return <ErrorPage />;
}
if (isError || !orders || orders.length === 0) {
return <EmptyOrders />;
}
return (
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6 text-gray-900">
{t("my_orders")}
</h1>
<Tabs defaultValue="active" className="w-full">
<TabsList className="mb-4 md:mb-6 w-full md:w-fit gap-2 p-1 bg-gray-100 rounded-xl">
<TabsTrigger
value="active"
className="rounded-lg font-semibold data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm"
>
{t("active_orders")} ({activeOrders.length})
</TabsTrigger>
<TabsTrigger
value="completed"
className="rounded-lg font-semibold data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm"
>
{t("completed_orders")} ({completedOrders.length})
</TabsTrigger>
</TabsList>
<TabsContent value="active">
{activeOrders.length === 0 ? (
<div className="flex items-center justify-center min-h-[40vh]">
<p className="text-xl text-gray-400 font-medium">
{t("no_active_orders")}
</p>
</div>
) : (
<div className="space-y-4">
{activeOrders.map((order) => (
<CompactOrderCard
key={order.id}
order={order}
isExpanded={expandedOrders.has(order.id)}
onToggle={() => toggleOrderExpand(order.id)}
onCancel={handleCancelOrder}
isCancelling={isCancellingOrder}
getStatusBadge={getStatusBadge}
calculateTotal={calculateTotal}
showCancelButton
t={t}
/>
))}
</div>
)}
</TabsContent>
<TabsContent value="completed">
{completedOrders.length === 0 ? (
<div className="flex items-center justify-center min-h-[40vh]">
<p className="text-xl text-gray-400 font-medium">
{t("no_completed_orders")}
</p>
</div>
) : (
<div className="space-y-4">
{completedOrders.map((order) => (
<CompactOrderCard
key={order.id}
order={order}
isExpanded={expandedOrders.has(order.id)}
onToggle={() => toggleOrderExpand(order.id)}
onCancel={handleCancelOrder}
isCancelling={isCancellingOrder}
getStatusBadge={getStatusBadge}
calculateTotal={calculateTotal}
showCancelButton={false}
t={t}
/>
))}
</div>
)}
</TabsContent>
</Tabs>
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
<DialogContent className="rounded-3xl border border-gray-200">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-900">
{t("cancel_order")} #{orderToCancel?.id}
</DialogTitle>
<DialogDescription className="text-gray-600">
{t("cancel_confirmation")}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setIsCancelDialogOpen(false)}
disabled={isCancellingOrder}
className="cursor-pointer rounded-xl border-2 border-gray-200 hover:border-gray-900 font-semibold"
>
{t("keep_order")}
</Button>
<Button
onClick={confirmCancelOrder}
disabled={isCancellingOrder}
className="cursor-pointer rounded-xl bg-red-600 hover:bg-red-700 font-semibold"
>
{isCancellingOrder ? t("cancelling") : t("cancel_order")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
interface CompactOrderCardProps {
order: Order;
isExpanded: boolean;
onToggle: () => void;
onCancel: (order: Order) => void;
isCancelling: boolean;
getStatusBadge: (status: string) => React.ReactNode;
calculateTotal: (order: Order) => number;
showCancelButton: boolean;
t: any;
}
function CompactOrderCard({
order,
isExpanded,
onToggle,
onCancel,
isCancelling,
getStatusBadge,
calculateTotal,
showCancelButton,
t,
}: CompactOrderCardProps) {
const total = useMemo(() => calculateTotal(order), [calculateTotal, order]);
const itemCount = order.orderItems.length;
return (
<Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-lg rounded-2xl border border-gray-200">
{/* Compact Header - Always Visible */}
<div
className="p-2 md:p-4 mx-2 md:mx-4 rounded-xl cursor-pointer bg-gradient-to-r from-white to-gray-50 hover:from-gray-50 hover:to-gray-100 transition-all duration-200"
onClick={onToggle}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<div className="flex items-center gap-2">
<div className="h-10 w-10 rounded-xl bg-gray-100 flex items-center justify-center">
<Package className="h-5 w-5 text-gray-700" />
</div>
<div>
<h3 className="font-bold text-base lg:text-lg text-gray-900">
{t("order_number")} {order.id}
</h3>
<p className="text-sm text-gray-500 font-medium">
{itemCount} {itemCount === 1 ? t("product") : t("products")}
</p>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col md:flex-row gap-2 items-end">
{getStatusBadge(order.status)}
<div className="text-right">
<p className="font-bold text-lg text-emerald-600">
{total.toFixed(2)} TMT
</p>
</div>
</div>
<div className="h-8 w-8 rounded-lg bg-gray-100 flex items-center justify-center">
{isExpanded ? (
<ChevronUp className="h-5 w-5 text-gray-600" />
) : (
<ChevronDown className="h-5 w-5 text-gray-600" />
)}
</div>
</div>
</div>
</div>
{/* Expandable Details */}
{isExpanded && (
<div className="border-t border-gray-200 bg-white">
{/* Order Info Grid */}
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50">
<div className="flex items-start gap-3">
<div className="h-9 w-9 rounded-xl bg-red-100 flex items-center justify-center shrink-0">
<MapPin className="h-5 w-5 text-red-600" />
</div>
<div>
<p className="text-sm font-bold text-gray-900">
{t("address")}
</p>
<p className="text-sm text-gray-600 mt-0.5">
{order.customer_address}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="h-9 w-9 rounded-xl bg-emerald-100 flex items-center justify-center shrink-0">
<CreditCard className="h-5 w-5 text-emerald-600" />
</div>
<div>
<p className="text-sm font-bold text-gray-900">
{t("payment_method")}
</p>
<p className="text-sm text-gray-600 mt-0.5">
{order.payment_type}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="h-9 w-9 rounded-xl bg-indigo-100 flex items-center justify-center shrink-0">
<ShoppingBag className="h-5 w-5 text-indigo-600" />
</div>
<div>
<p className="text-sm font-bold text-gray-900">
{t("shipping_method")}
</p>
<p className="text-sm text-gray-600 mt-0.5">
{order.shipping_method}
</p>
</div>
</div>
</div>
{/* Products List */}
<div className="p-4">
<h4 className="font-bold mb-3 text-gray-900">{t("products")}:</h4>
<div className="space-y-3 max-h-96 overflow-y-auto">
{order.orderItems.map((item, index) => (
<div
key={index}
className="flex items-center gap-4 p-3 bg-gray-50 rounded-xl hover:bg-gray-100 transition-colors border border-gray-100"
>
<div className="relative w-16 h-16 shrink-0 rounded-xl overflow-hidden bg-white border border-gray-200">
<Image
src={
item.product.images_400x400 || item.product.thumbnail
}
alt={item.product.name}
fill
className="object-contain p-1"
/>
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-sm line-clamp-2 text-gray-900">
{item.product.name}
</p>
<p className="text-xs text-gray-500 mt-1 font-medium">
{item.quantity} × {item.unit_price_amount} TMT
</p>
</div>
<div className="text-right">
<p className="font-bold text-sm text-gray-900">
{(
parseFloat(item.unit_price_amount) * item.quantity
).toFixed(2)}{" "}
TMT
</p>
</div>
</div>
))}
</div>
</div>
{/* Footer with Total and Actions */}
<div className="border-t border-gray-200 p-4 bg-gray-50">
<div className="flex items-center justify-between mb-3">
<span className="text-base font-bold text-gray-900">
{t("total_price")}:
</span>
<span className="text-xl font-bold text-emerald-600">
{total.toFixed(2)} TMT
</span>
</div>
{showCancelButton && (
<Button
onClick={(e) => {
e.stopPropagation();
onCancel(order);
}}
disabled={isCancelling}
className="w-full cursor-pointer rounded-xl bg-red-600 hover:bg-red-700 font-semibold h-11"
>
{t("cancel_order")}
</Button>
)}
</div>
</div>
)}
</Card>
);
}