Fix product stock, booking actions, settings, reservation UX, nav and category filter

This commit is contained in:
belviskhoremk
2026-05-20 23:56:43 +00:00
parent a89793a059
commit affff1c502
12 changed files with 137 additions and 102 deletions

View File

@@ -110,7 +110,7 @@ export default function AdminClients() {
) : ( ) : (
customers.map((c) => ( customers.map((c) => (
<TableRow key={c.id}> <TableRow key={c.id}>
<TableCell className="font-medium">{c.full_name ?? "—"}</TableCell> <TableCell className="font-medium">{c.full_name || "—"}</TableCell>
<TableCell className="text-sm text-muted-foreground">{c.email}</TableCell> <TableCell className="text-sm text-muted-foreground">{c.email}</TableCell>
<TableCell className="text-sm text-muted-foreground">{c.phone ?? "—"}</TableCell> <TableCell className="text-sm text-muted-foreground">{c.phone ?? "—"}</TableCell>
<TableCell className="text-center text-sm">{c.orders_count}</TableCell> <TableCell className="text-center text-sm">{c.orders_count}</TableCell>

View File

@@ -6,22 +6,27 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { toast } from "sonner"; import { toast } from "sonner";
import { adminGetSettings, adminUpdateSetting, StoreSetting } from "@/lib/api/settings"; import { adminGetSettings, adminUpdateSetting } from "@/lib/api/settings";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { ApiError } from "@/lib/api"; import { ApiError } from "@/lib/api";
import { Save } from "lucide-react"; import { Save } from "lucide-react";
const SETTING_KEYS = ["default_booking_price"] as const;
type SettingKey = typeof SETTING_KEYS[number];
interface SettingMeta { label: string; description: string; type: "number" | "text" }
export default function AdminParametres() { export default function AdminParametres() {
const { t, locale } = useLanguage(); const { t, locale } = useLanguage();
const [settings, setSettings] = useState<StoreSetting[]>([]); const [values, setValues] = useState<Record<string, string>>({ default_booking_price: "0" });
const [values, setValues] = useState<Record<string, string>>({}); const [updatedAt, setUpdatedAt] = useState<Record<string, string>>({});
const [saving, setSaving] = useState<string | null>(null); const [saving, setSaving] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const SETTING_META: Record<string, { label: string; description: string; type: "number" | "text" }> = { const SETTING_META: Record<SettingKey, SettingMeta> = {
default_booking_price: { default_booking_price: {
label: t("booking.free") === "Free" ? "Default booking price (€)" : t("booking.free") === "Kostenlos" ? "Standardpreis für Reservierungen (€)" : "Prix de réservation par défaut (€)", label: t("admin.settings.booking_price_label"),
description: t("booking.free") === "Free" ? "Price applied to each appointment (0 = free)" : t("booking.free") === "Kostenlos" ? "Preis pro Termin (0 = kostenlos)" : "Prix appliqué à chaque rendez-vous (0 = gratuit)", description: t("admin.settings.booking_price_desc"),
type: "number", type: "number",
}, },
}; };
@@ -29,12 +34,14 @@ export default function AdminParametres() {
useEffect(() => { useEffect(() => {
adminGetSettings() adminGetSettings()
.then((rows) => { .then((rows) => {
setSettings(rows); const vals: Record<string, string> = { default_booking_price: "0" };
const init: Record<string, string> = {}; const dates: Record<string, string> = {};
rows.forEach((r) => { rows.forEach((r) => {
init[r.key] = r.value !== null && r.value !== undefined ? String(r.value) : ""; vals[r.key] = r.value !== null && r.value !== undefined ? String(r.value) : "";
if (r.updated_at) dates[r.key] = r.updated_at;
}); });
setValues(init); setValues(vals);
setUpdatedAt(dates);
}) })
.catch((e) => toast.error(e instanceof ApiError ? e.message : t("admin.error"))) .catch((e) => toast.error(e instanceof ApiError ? e.message : t("admin.error")))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -43,10 +50,11 @@ export default function AdminParametres() {
const handleSave = async (key: string) => { const handleSave = async (key: string) => {
setSaving(key); setSaving(key);
try { try {
const meta = SETTING_META[key]; const meta = SETTING_META[key as SettingKey];
const raw = values[key] ?? ""; const raw = values[key] ?? "";
const value = meta?.type === "number" ? parseFloat(raw) : raw; const value = meta?.type === "number" ? parseFloat(raw) || 0 : raw;
await adminUpdateSetting(key, value); const saved = await adminUpdateSetting(key, value);
if (saved.updated_at) setUpdatedAt((prev) => ({ ...prev, [key]: saved.updated_at }));
toast.success(t("admin.settings.saved")); toast.success(t("admin.settings.saved"));
} catch (err) { } catch (err) {
toast.error(err instanceof ApiError ? err.message : t("admin.error")); toast.error(err instanceof ApiError ? err.message : t("admin.error"));
@@ -55,9 +63,6 @@ export default function AdminParametres() {
} }
}; };
const knownKeys = settings.filter((s) => SETTING_META[s.key]);
const unknownKeys = settings.filter((s) => !SETTING_META[s.key]);
return ( return (
<div className="space-y-8 max-w-2xl"> <div className="space-y-8 max-w-2xl">
<div> <div>
@@ -67,76 +72,46 @@ export default function AdminParametres() {
{loading ? ( {loading ? (
<div className="space-y-4"> <div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" /> <div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
))} ))}
</div> </div>
) : ( ) : (
<>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base">{t("admin.settings.bookings")}</CardTitle> <CardTitle className="text-base">{t("admin.settings.bookings")}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{knownKeys.map((s) => { {SETTING_KEYS.map((key) => {
const meta = SETTING_META[s.key]; const meta = SETTING_META[key];
return ( return (
<div key={s.key}> <div key={key}>
<Label htmlFor={s.key}>{meta.label}</Label> <Label htmlFor={key}>{meta.label}</Label>
<p className="text-xs text-muted-foreground mb-2">{meta.description}</p> <p className="text-xs text-muted-foreground mb-2">{meta.description}</p>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id={s.key} id={key}
type={meta.type} type={meta.type}
value={values[s.key] ?? ""} min={meta.type === "number" ? 0 : undefined}
onChange={(e) => setValues((prev) => ({ ...prev, [s.key]: e.target.value }))} value={values[key] ?? ""}
onChange={(e) => setValues((prev) => ({ ...prev, [key]: e.target.value }))}
className="max-w-[200px]" className="max-w-[200px]"
/> />
<Button size="sm" onClick={() => handleSave(s.key)} disabled={saving === s.key}> <Button size="sm" onClick={() => handleSave(key)} disabled={saving === key}>
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
{saving === s.key ? t("admin.saving") : t("admin.save")} {saving === key ? t("admin.saving") : t("admin.save")}
</Button> </Button>
</div> </div>
{s.updated_at && ( {updatedAt[key] && (
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{t("admin.settings.last_updated")} {new Date(s.updated_at).toLocaleDateString(locale)} {t("admin.settings.last_updated")} {new Date(updatedAt[key]).toLocaleDateString(locale)}
</p> </p>
)} )}
</div> </div>
); );
})} })}
{knownKeys.length === 0 && (
<p className="text-sm text-muted-foreground">{t("admin.settings.no_settings")}</p>
)}
</CardContent> </CardContent>
</Card> </Card>
{unknownKeys.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base">{t("admin.settings.other")}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{unknownKeys.map((s) => (
<div key={s.key}>
<Label htmlFor={`u-${s.key}`}>{s.key}</Label>
<div className="flex gap-2 mt-1">
<Input
id={`u-${s.key}`}
value={values[s.key] ?? ""}
onChange={(e) => setValues((prev) => ({ ...prev, [s.key]: e.target.value }))}
/>
<Button size="sm" onClick={() => handleSave(s.key)} disabled={saving === s.key}>
<Save className="h-4 w-4 mr-2" />
{saving === s.key ? t("admin.saving") : t("admin.save")}
</Button>
</div>
</div>
))}
</CardContent>
</Card>
)}
</>
)} )}
</div> </div>
); );

View File

@@ -33,6 +33,7 @@ type FormState = {
category: Product["category"]; category: Product["category"];
price: string; price: string;
original_price: string; original_price: string;
stock_quantity: string;
description: string; description: string;
colors: string; colors: string;
lengths: string; lengths: string;
@@ -45,6 +46,7 @@ const emptyForm: FormState = {
category: "clip-in", category: "clip-in",
price: "", price: "",
original_price: "", original_price: "",
stock_quantity: "0",
description: "", description: "",
colors: "", colors: "",
lengths: "", lengths: "",
@@ -86,6 +88,7 @@ export default function AdminProducts() {
category: p.category, category: p.category,
price: String(p.price), price: String(p.price),
original_price: p.originalPrice ? String(p.originalPrice) : "", original_price: p.originalPrice ? String(p.originalPrice) : "",
stock_quantity: String(p.stockQuantity ?? 0),
description: p.description, description: p.description,
colors: p.colors.join(", "), colors: p.colors.join(", "),
lengths: p.lengths.join(", "), lengths: p.lengths.join(", "),
@@ -123,6 +126,7 @@ export default function AdminProducts() {
category: form.category, category: form.category,
price, price,
original_price: form.original_price ? parseFloat(form.original_price) : undefined, original_price: form.original_price ? parseFloat(form.original_price) : undefined,
stock_quantity: parseInt(form.stock_quantity) || 0,
colors: form.colors.split(",").map((c) => c.trim()).filter(Boolean), colors: form.colors.split(",").map((c) => c.trim()).filter(Boolean),
lengths: form.lengths.split(",").map((l) => l.trim()).filter(Boolean), lengths: form.lengths.split(",").map((l) => l.trim()).filter(Boolean),
description: form.description, description: form.description,
@@ -195,6 +199,7 @@ export default function AdminProducts() {
<TableHead>{t("admin.products.col_name")}</TableHead> <TableHead>{t("admin.products.col_name")}</TableHead>
<TableHead>{t("admin.products.col_category")}</TableHead> <TableHead>{t("admin.products.col_category")}</TableHead>
<TableHead>{t("admin.products.col_price")}</TableHead> <TableHead>{t("admin.products.col_price")}</TableHead>
<TableHead className="text-center">{t("admin.products.col_stock")}</TableHead>
<TableHead>{t("admin.products.col_status")}</TableHead> <TableHead>{t("admin.products.col_status")}</TableHead>
<TableHead className="text-right">{t("admin.actions")}</TableHead> <TableHead className="text-right">{t("admin.actions")}</TableHead>
</TableRow> </TableRow>
@@ -214,6 +219,9 @@ export default function AdminProducts() {
<TableCell className="font-medium">{p.name}</TableCell> <TableCell className="font-medium">{p.name}</TableCell>
<TableCell className="text-muted-foreground">{categoryLabels[p.category]}</TableCell> <TableCell className="text-muted-foreground">{categoryLabels[p.category]}</TableCell>
<TableCell>{p.price} </TableCell> <TableCell>{p.price} </TableCell>
<TableCell className="text-center">
<span className={p.stockQuantity === 0 ? "text-destructive font-medium" : ""}>{p.stockQuantity ?? 0}</span>
</TableCell>
<TableCell className="space-x-1"> <TableCell className="space-x-1">
{p.isNew && <Badge variant="secondary">{t("admin.products.badge_new")}</Badge>} {p.isNew && <Badge variant="secondary">{t("admin.products.badge_new")}</Badge>}
{p.isBestseller && <Badge>Bestseller</Badge>} {p.isBestseller && <Badge>Bestseller</Badge>}
@@ -265,6 +273,10 @@ export default function AdminProducts() {
<Label htmlFor="original_price">{t("admin.products.original_price")}</Label> <Label htmlFor="original_price">{t("admin.products.original_price")}</Label>
<Input id="original_price" type="number" step="0.01" value={form.original_price} onChange={(e) => setForm({ ...form, original_price: e.target.value })} placeholder={t("admin.products.optional")} /> <Input id="original_price" type="number" step="0.01" value={form.original_price} onChange={(e) => setForm({ ...form, original_price: e.target.value })} placeholder={t("admin.products.optional")} />
</div> </div>
<div className="space-y-2 col-span-2">
<Label htmlFor="stock_quantity">{t("admin.products.stock_quantity")}</Label>
<Input id="stock_quantity" type="number" min="0" step="1" value={form.stock_quantity} onChange={(e) => setForm({ ...form, stock_quantity: e.target.value })} />
</div>
<div className="space-y-2 col-span-2"> <div className="space-y-2 col-span-2">
<Label>{t("admin.products.image")}</Label> <Label>{t("admin.products.image")}</Label>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">

View File

@@ -1,18 +1,25 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { Suspense, useState, useEffect } from "react";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { useSearchParams, useRouter } from "next/navigation";
import { categories } from "@/data/products"; import { categories } from "@/data/products";
import type { Product } from "@/data/products"; import type { Product } from "@/data/products";
import { listProducts } from "@/lib/api/products"; import { listProducts } from "@/lib/api/products";
import ProductCard from "@/components/ProductCard"; import ProductCard from "@/components/ProductCard";
export default function Shop() { function ShopContent() {
const { t } = useLanguage(); const { t } = useLanguage();
const [selectedCategory, setSelectedCategory] = useState("all"); const searchParams = useSearchParams();
const router = useRouter();
const selectedCategory = searchParams.get("category") || "all";
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const setCategory = (cat: string) => {
router.push(cat === "all" ? "/boutique" : `/boutique?category=${cat}`);
};
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
listProducts({ listProducts({
@@ -31,7 +38,7 @@ export default function Shop() {
<div className="flex flex-wrap justify-center gap-2 mb-10"> <div className="flex flex-wrap justify-center gap-2 mb-10">
<button <button
onClick={() => setSelectedCategory("all")} onClick={() => setCategory("all")}
className={`px-4 py-2 rounded-full text-sm transition-colors ${ className={`px-4 py-2 rounded-full text-sm transition-colors ${
selectedCategory === "all" selectedCategory === "all"
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
@@ -43,7 +50,7 @@ export default function Shop() {
{categories.map((cat) => ( {categories.map((cat) => (
<button <button
key={cat.id} key={cat.id}
onClick={() => setSelectedCategory(cat.id)} onClick={() => setCategory(cat.id)}
className={`px-4 py-2 rounded-full text-sm transition-colors cursor-pointer ${ className={`px-4 py-2 rounded-full text-sm transition-colors cursor-pointer ${
selectedCategory === cat.id selectedCategory === cat.id
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
@@ -74,3 +81,11 @@ export default function Shop() {
</div> </div>
); );
} }
export default function Shop() {
return (
<Suspense>
<ShopContent />
</Suspense>
);
}

View File

@@ -21,7 +21,9 @@ export default function Auth() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (user) router.replace("/"); if (user) {
router.replace(user.role === "admin" ? "/admin" : "/");
}
}, [user, router]); }, [user, router]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@@ -29,12 +31,19 @@ export default function Auth() {
setLoading(true); setLoading(true);
try { try {
if (isLogin) { if (isLogin) {
await login(email, password); const profile = await login(email, password);
toast.success(t("auth.login_success"));
router.push(profile.role === "admin" ? "/admin" : "/");
} else { } else {
await register(email, password, name); const profile = await register(email, password, name);
if (profile) {
toast.success(t("auth.register_success"));
router.push(profile.role === "admin" ? "/admin" : "/");
} else {
toast.success(t("auth.confirm_email"));
router.push("/connexion");
}
} }
toast.success(isLogin ? t("auth.login_success") : t("auth.register_success"));
setTimeout(() => router.push("/"), 800);
} catch (err) { } catch (err) {
const msg = err instanceof ApiError ? err.message : t("auth.error"); const msg = err instanceof ApiError ? err.message : t("auth.error");
toast.error(msg); toast.error(msg);

View File

@@ -81,6 +81,15 @@ export default function Booking() {
} }
}; };
const resetBooking = () => {
setConfirmed(false);
setSelectedService(null);
setSelectedDate(undefined);
setSelectedSlot(null);
setSlots([]);
setPhone(user?.phone ?? "");
};
if (confirmed) { if (confirmed) {
return ( return (
<div className="min-h-screen flex items-center justify-center py-12 px-4"> <div className="min-h-screen flex items-center justify-center py-12 px-4">
@@ -92,9 +101,12 @@ export default function Booking() {
<p className="text-muted-foreground mb-2"> <p className="text-muted-foreground mb-2">
{selectedService?.name} {selectedDate?.toLocaleDateString(locale)} {t("booking.confirmed_at")} {selectedSlot?.start_time.slice(0, 5)} {selectedService?.name} {selectedDate?.toLocaleDateString(locale)} {t("booking.confirmed_at")} {selectedSlot?.start_time.slice(0, 5)}
</p> </p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground mb-6">
{t("booking.confirmed_desc")} {t("booking.confirmed_desc")}
</p> </p>
<Button variant="outline" onClick={resetBooking}>
{t("booking.new_booking")}
</Button>
</div> </div>
</div> </div>
); );

View File

@@ -17,6 +17,7 @@ export default function Header() {
const currentPath = usePathname(); const currentPath = usePathname();
const navLinks = [ const navLinks = [
{ to: "/", label: t("nav.home") },
{ to: "/boutique", label: t("nav.shop") }, { to: "/boutique", label: t("nav.shop") },
{ to: "/reservation", label: t("nav.booking") }, { to: "/reservation", label: t("nav.booking") },
{ to: "/a-propos", label: t("nav.about") }, { to: "/a-propos", label: t("nav.about") },

View File

@@ -11,7 +11,7 @@ interface AuthContextType {
isLoading: boolean; isLoading: boolean;
isAdmin: boolean; isAdmin: boolean;
login: (email: string, password: string) => Promise<UserProfile>; login: (email: string, password: string) => Promise<UserProfile>;
register: (email: string, password: string, name: string) => Promise<UserProfile>; register: (email: string, password: string, name: string) => Promise<UserProfile | null>;
logout: () => void; logout: () => void;
} }
@@ -39,9 +39,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
return profile; return profile;
}; };
const register = async (email: string, password: string, name: string): Promise<UserProfile> => { const register = async (email: string, password: string, name: string): Promise<UserProfile | null> => {
const profile = await authApi.register(email, password, name); const profile = await authApi.register(email, password, name);
setUser(profile); if (profile) setUser(profile);
return profile; return profile;
}; };

View File

@@ -13,6 +13,8 @@ interface LanguageContextType {
const translations: Record<string, Record<Language, string>> = { const translations: Record<string, Record<Language, string>> = {
// ── Public nav ──────────────────────────────────────────────────────────────── // ── Public nav ────────────────────────────────────────────────────────────────
"nav.home": { fr: "Accueil", de: "Startseite", en: "Home" },
"auth.confirm_email": { fr: "Vérifiez votre email pour confirmer votre compte", de: "Überprüfen Sie Ihre E-Mail, um Ihr Konto zu bestätigen", en: "Check your email to confirm your account" },
"nav.shop": { fr: "Boutique", de: "Shop", en: "Shop" }, "nav.shop": { fr: "Boutique", de: "Shop", en: "Shop" },
"nav.booking": { fr: "Réservation", de: "Terminbuchung", en: "Book Appointment" }, "nav.booking": { fr: "Réservation", de: "Terminbuchung", en: "Book Appointment" },
"nav.about": { fr: "À propos", de: "Über uns", en: "About" }, "nav.about": { fr: "À propos", de: "Über uns", en: "About" },
@@ -94,6 +96,7 @@ const translations: Record<string, Record<Language, string>> = {
"booking.confirmed_desc": { fr: "Vous recevrez une confirmation par email. Nous vous contacterons pour finaliser le paiement.", de: "Sie erhalten eine Bestätigungs-E-Mail. Wir werden Sie kontaktieren, um die Zahlung abzuschließen.", en: "You will receive a confirmation email. We will contact you to finalize the payment." }, "booking.confirmed_desc": { fr: "Vous recevrez une confirmation par email. Nous vous contacterons pour finaliser le paiement.", de: "Sie erhalten eine Bestätigungs-E-Mail. Wir werden Sie kontaktieren, um die Zahlung abzuschließen.", en: "You will receive a confirmation email. We will contact you to finalize the payment." },
"booking.no_services": { fr: "Aucun service disponible pour le moment.", de: "Derzeit keine Dienstleistungen verfügbar.", en: "No services available at the moment." }, "booking.no_services": { fr: "Aucun service disponible pour le moment.", de: "Derzeit keine Dienstleistungen verfügbar.", en: "No services available at the moment." },
"booking.no_slots": { fr: "Aucun créneau disponible ce jour. Choisissez une autre date.", de: "Keine Zeitfenster an diesem Tag. Wählen Sie ein anderes Datum.", en: "No slots available this day. Choose another date." }, "booking.no_slots": { fr: "Aucun créneau disponible ce jour. Choisissez une autre date.", de: "Keine Zeitfenster an diesem Tag. Wählen Sie ein anderes Datum.", en: "No slots available this day. Choose another date." },
"booking.new_booking": { fr: "Faire une nouvelle réservation", de: "Neue Buchung", en: "Make another booking" },
"booking.submitting": { fr: "Envoi en cours…", de: "Wird gesendet…", en: "Submitting…" }, "booking.submitting": { fr: "Envoi en cours…", de: "Wird gesendet…", en: "Submitting…" },
"booking.success": { fr: "Réservation confirmée !", de: "Buchung bestätigt!", en: "Booking confirmed!" }, "booking.success": { fr: "Réservation confirmée !", de: "Buchung bestätigt!", en: "Booking confirmed!" },
"booking.error": { fr: "Erreur lors de la réservation", de: "Buchungsfehler", en: "Booking error" }, "booking.error": { fr: "Erreur lors de la réservation", de: "Buchungsfehler", en: "Booking error" },
@@ -298,6 +301,7 @@ const translations: Record<string, Record<Language, string>> = {
"admin.products.col_name": { fr: "Nom", de: "Name", en: "Name" }, "admin.products.col_name": { fr: "Nom", de: "Name", en: "Name" },
"admin.products.col_category": { fr: "Catégorie", de: "Kategorie", en: "Category" }, "admin.products.col_category": { fr: "Catégorie", de: "Kategorie", en: "Category" },
"admin.products.col_price": { fr: "Prix", de: "Preis", en: "Price" }, "admin.products.col_price": { fr: "Prix", de: "Preis", en: "Price" },
"admin.products.col_stock": { fr: "Stock", de: "Bestand", en: "Stock" },
"admin.products.col_status": { fr: "Statut", de: "Status", en: "Status" }, "admin.products.col_status": { fr: "Statut", de: "Status", en: "Status" },
"admin.products.badge_new": { fr: "Nouveau", de: "Neu", en: "New" }, "admin.products.badge_new": { fr: "Nouveau", de: "Neu", en: "New" },
"admin.products.create_title": { fr: "Nouveau produit", de: "Neues Produkt", en: "New product" }, "admin.products.create_title": { fr: "Nouveau produit", de: "Neues Produkt", en: "New product" },
@@ -307,6 +311,7 @@ const translations: Record<string, Record<Language, string>> = {
"admin.products.category": { fr: "Catégorie", de: "Kategorie", en: "Category" }, "admin.products.category": { fr: "Catégorie", de: "Kategorie", en: "Category" },
"admin.products.price": { fr: "Prix (€)", de: "Preis (€)", en: "Price (€)" }, "admin.products.price": { fr: "Prix (€)", de: "Preis (€)", en: "Price (€)" },
"admin.products.original_price": { fr: "Prix barré (€)", de: "Durchgestrichener Preis (€)", en: "Original price (€)" }, "admin.products.original_price": { fr: "Prix barré (€)", de: "Durchgestrichener Preis (€)", en: "Original price (€)" },
"admin.products.stock_quantity": { fr: "Quantité en stock", de: "Lagerbestand", en: "Stock quantity" },
"admin.products.optional": { fr: "Optionnel", de: "Optional", en: "Optional" }, "admin.products.optional": { fr: "Optionnel", de: "Optional", en: "Optional" },
"admin.products.image": { fr: "Image", de: "Bild", en: "Image" }, "admin.products.image": { fr: "Image", de: "Bild", en: "Image" },
"admin.products.description": { fr: "Description", de: "Beschreibung", en: "Description" }, "admin.products.description": { fr: "Description", de: "Beschreibung", en: "Description" },
@@ -368,6 +373,8 @@ const translations: Record<string, Record<Language, string>> = {
"admin.settings.other": { fr: "Autres paramètres", de: "Weitere Einstellungen", en: "Other settings" }, "admin.settings.other": { fr: "Autres paramètres", de: "Weitere Einstellungen", en: "Other settings" },
"admin.settings.last_updated": { fr: "Dernière modification :", de: "Zuletzt geändert:", en: "Last updated:" }, "admin.settings.last_updated": { fr: "Dernière modification :", de: "Zuletzt geändert:", en: "Last updated:" },
"admin.settings.saved": { fr: "Paramètre enregistré", de: "Einstellung gespeichert", en: "Setting saved" }, "admin.settings.saved": { fr: "Paramètre enregistré", de: "Einstellung gespeichert", en: "Setting saved" },
"admin.settings.booking_price_label": { fr: "Prix de réservation par défaut (€)", de: "Standardpreis für Reservierungen (€)", en: "Default booking price (€)" },
"admin.settings.booking_price_desc": { fr: "Prix appliqué à chaque rendez-vous (0 = gratuit)", de: "Preis pro Termin (0 = kostenlos)", en: "Price applied to each appointment (0 = free)" },
}; };
const LOCALE_MAP: Record<Language, string> = { const LOCALE_MAP: Record<Language, string> = {

View File

@@ -14,6 +14,7 @@ export interface Product {
isBestseller?: boolean; isBestseller?: boolean;
rating: number; rating: number;
reviewCount: number; reviewCount: number;
stockQuantity?: number;
} }
export const products: Product[] = [ export const products: Product[] = [

View File

@@ -25,12 +25,14 @@ export async function register(
email: string, email: string,
password: string, password: string,
name: string name: string
): Promise<UserProfile> { ): Promise<UserProfile | null> {
const tokens = await api.post<AuthResponse>("/auth/register", { email, password, name }); const res = await api.post<AuthResponse | { message: string }>("/auth/register", { email, password, name });
if ("access_token" in (tokens as object)) { if ("access_token" in res) {
setTokens((tokens as AuthResponse).access_token, (tokens as AuthResponse).refresh_token); setTokens(res.access_token, (res as AuthResponse).refresh_token);
}
return api.get<UserProfile>("/auth/me"); return api.get<UserProfile>("/auth/me");
}
// Email confirmation required — no session yet
return null;
} }
export async function getMe(): Promise<UserProfile> { export async function getMe(): Promise<UserProfile> {

View File

@@ -40,6 +40,7 @@ function toProduct(p: ApiProduct): Product {
isBestseller: p.is_bestseller, isBestseller: p.is_bestseller,
rating: p.rating, rating: p.rating,
reviewCount: p.review_count, reviewCount: p.review_count,
stockQuantity: p.stock_quantity,
}; };
} }