mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-13 11:03:02 +00:00
270 lines
11 KiB
TypeScript
270 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, refreshUser } = 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]);
|
|
|
|
// Refresh profile from server on mount to get the latest full_name
|
|
useEffect(() => {
|
|
if (user) refreshUser().catch(() => {});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
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);
|
|
await refreshUser();
|
|
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>
|
|
);
|
|
}
|