Files
badoHair_fe/app/mon-compte/page.tsx
2026-05-24 18:50:28 +00:00

263 lines
11 KiB
TypeScript

"use client";
import { useState, useEffect, startTransition } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/contexts/AuthContext";
import { useLanguage } from "@/contexts/LanguageContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { updateProfile } from "@/lib/api/auth";
import { listMyBookings, cancelBooking, BookingApi } from "@/lib/api/bookings";
import { listMyOrders, MyOrderApi } from "@/lib/api/orders";
import { ApiError } from "@/lib/api";
import { toast } from "sonner";
import { User, CalendarDays, ShoppingBag, X } from "lucide-react";
export default function MonCompte() {
const { user, isLoading } = useAuth();
const { t, locale } = useLanguage();
const router = useRouter();
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [savingProfile, setSavingProfile] = useState(false);
const [bookings, setBookings] = useState<BookingApi[]>([]);
const [bookingsLoading, setBookingsLoading] = useState(true);
const [orders, setOrders] = useState<MyOrderApi[]>([]);
const [ordersLoading, setOrdersLoading] = useState(true);
const [cancellingId, setCancellingId] = useState<string | null>(null);
useEffect(() => {
if (!isLoading && !user) router.replace("/connexion");
if (!isLoading && user?.role === "admin") router.replace("/admin");
}, [user, isLoading, router]);
useEffect(() => {
if (user) {
startTransition(() => {
setName(user.full_name ?? "");
setPhone(user.phone ?? "");
});
}
}, [user]);
useEffect(() => {
if (!user) return;
listMyBookings()
.then((res) => setBookings(res.data))
.catch(() => setBookings([]))
.finally(() => setBookingsLoading(false));
listMyOrders()
.then((res) => setOrders(res.data))
.catch(() => setOrders([]))
.finally(() => setOrdersLoading(false));
}, [user]);
const handleSaveProfile = async (e: React.FormEvent) => {
e.preventDefault();
setSavingProfile(true);
try {
await updateProfile(name, phone || null);
toast.success(t("account.profile_saved"));
} catch (err) {
toast.error(err instanceof ApiError ? err.message : t("auth.error"));
} finally {
setSavingProfile(false);
}
};
const handleCancel = async (id: string) => {
setCancellingId(id);
try {
await cancelBooking(id);
setBookings((prev) => prev.map((b) => b.id === id ? { ...b, status: "cancelled" } : b));
toast.success(t("account.booking_cancelled"));
} catch (err) {
toast.error(err instanceof ApiError ? err.message : t("auth.error"));
} finally {
setCancellingId(null);
}
};
if (isLoading || !user) return null;
const bookingStatusConfig: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
pending: { label: t("admin.status.pending"), variant: "secondary" },
confirmed: { label: t("admin.status.confirmed"), variant: "default" },
cancelled: { label: t("admin.status.cancelled"), variant: "destructive" },
completed: { label: t("admin.status.completed"), variant: "outline" },
no_show: { label: t("admin.status.no_show"), variant: "destructive" },
};
const orderStatusConfig: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
pending: { label: t("admin.status.pending"), variant: "secondary" },
paid: { label: t("admin.status.paid"), variant: "default" },
shipped: { label: t("admin.status.shipped"), variant: "outline" },
delivered: { label: t("admin.status.delivered"), variant: "default" },
cancelled: { label: t("admin.status.cancelled"), variant: "destructive" },
refunded: { label: t("admin.status.refunded"), variant: "destructive" },
};
return (
<div className="min-h-screen py-12 px-4">
<div className="container mx-auto max-w-3xl">
<h1 className="font-serif text-3xl lg:text-4xl mb-8 text-center">{t("account.title")}</h1>
<Tabs defaultValue="profil">
<TabsList className="w-full mb-8">
<TabsTrigger value="profil" className="flex-1">
<User className="h-4 w-4 mr-2" /> {t("account.tab_profile")}
</TabsTrigger>
<TabsTrigger value="reservations" className="flex-1">
<CalendarDays className="h-4 w-4 mr-2" /> {t("account.tab_bookings")}
</TabsTrigger>
<TabsTrigger value="commandes" className="flex-1">
<ShoppingBag className="h-4 w-4 mr-2" /> {t("account.tab_orders")}
</TabsTrigger>
</TabsList>
<TabsContent value="profil">
<Card>
<CardHeader>
<CardTitle className="font-serif text-xl">{t("account.personal_info")}</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSaveProfile} className="space-y-4 max-w-sm">
<div>
<Label htmlFor="email">{t("auth.email")}</Label>
<Input id="email" value={user.email} disabled className="mt-1" />
</div>
<div>
<Label htmlFor="name">{t("auth.name")}</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="phone">{t("booking.phone")}</Label>
<Input
id="phone"
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="mt-1"
/>
</div>
<Button type="submit" disabled={savingProfile}>
{savingProfile ? t("admin.saving") : t("admin.save")}
</Button>
</form>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="reservations">
<Card>
<CardHeader>
<CardTitle className="font-serif text-xl">{t("account.tab_bookings")}</CardTitle>
</CardHeader>
<CardContent>
{bookingsLoading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
))}
</div>
) : bookings.length === 0 ? (
<p className="text-sm text-muted-foreground py-6 text-center">
{t("account.no_bookings")}
</p>
) : (
<div className="space-y-3">
{bookings.map((b) => {
const cfg = bookingStatusConfig[b.status] ?? { label: b.status, variant: "outline" as const };
const canCancel = b.status === "pending" || b.status === "confirmed";
return (
<div key={b.id} className="flex items-center justify-between p-4 bg-muted/40 rounded-lg border border-border">
<div>
<div className="font-medium text-sm">{b.service_note ?? t("account.appt_default")}</div>
<div className="text-xs text-muted-foreground mt-0.5">
{new Date(b.slot_date + "T00:00:00").toLocaleDateString(locale, { day: "2-digit", month: "short", year: "numeric" })} {t("booking.confirmed_at")} {b.slot_start}
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant={cfg.variant}>{cfg.label}</Badge>
{canCancel && (
<Button
variant="ghost"
size="icon"
onClick={() => handleCancel(b.id)}
disabled={cancellingId === b.id}
title={t("account.cancel")}
>
<X className="h-4 w-4 text-muted-foreground" />
</Button>
)}
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="commandes">
<Card>
<CardHeader>
<CardTitle className="font-serif text-xl">{t("account.tab_orders")}</CardTitle>
</CardHeader>
<CardContent>
{ordersLoading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
))}
</div>
) : orders.length === 0 ? (
<p className="text-sm text-muted-foreground py-6 text-center">
{t("account.no_orders")}
</p>
) : (
<div className="space-y-3">
{orders.map((o) => {
const cfg = orderStatusConfig[o.status] ?? { label: o.status, variant: "outline" as const };
return (
<div key={o.id} className="flex items-center justify-between p-4 bg-muted/40 rounded-lg border border-border">
<div>
<div className="font-mono text-xs text-muted-foreground">
#{o.id.slice(0, 8).toUpperCase()}
</div>
<div className="text-sm font-medium mt-0.5">{o.total_amount.toFixed(2)} </div>
<div className="text-xs text-muted-foreground">
{new Date(o.created_at).toLocaleDateString(locale, {
day: "2-digit", month: "short", year: "numeric",
})}
</div>
</div>
<Badge variant={cfg.variant}>{cfg.label}</Badge>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
}