mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-13 08:58:31 +00:00
Update May 12 by Elvis
This commit is contained in:
259
app/mon-compte/page.tsx
Normal file
259
app/mon-compte/page.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } 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");
|
||||
}, [user, isLoading, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user