mirror of
http://88.130.71.182:3000/BlitTech/badoHair_fe.git
synced 2026-06-12 23:23:22 +00:00
Fix product stock, booking actions, settings, reservation UX, nav and category filter
This commit is contained in:
@@ -110,7 +110,7 @@ export default function AdminClients() {
|
||||
) : (
|
||||
customers.map((c) => (
|
||||
<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.phone ?? "—"}</TableCell>
|
||||
<TableCell className="text-center text-sm">{c.orders_count}</TableCell>
|
||||
|
||||
@@ -6,22 +6,27 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
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 { ApiError } from "@/lib/api";
|
||||
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() {
|
||||
const { t, locale } = useLanguage();
|
||||
const [settings, setSettings] = useState<StoreSetting[]>([]);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [values, setValues] = useState<Record<string, string>>({ default_booking_price: "0" });
|
||||
const [updatedAt, setUpdatedAt] = useState<Record<string, string>>({});
|
||||
const [saving, setSaving] = useState<string | null>(null);
|
||||
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: {
|
||||
label: t("booking.free") === "Free" ? "Default booking price (€)" : t("booking.free") === "Kostenlos" ? "Standardpreis für Reservierungen (€)" : "Prix de réservation par défaut (€)",
|
||||
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)",
|
||||
label: t("admin.settings.booking_price_label"),
|
||||
description: t("admin.settings.booking_price_desc"),
|
||||
type: "number",
|
||||
},
|
||||
};
|
||||
@@ -29,12 +34,14 @@ export default function AdminParametres() {
|
||||
useEffect(() => {
|
||||
adminGetSettings()
|
||||
.then((rows) => {
|
||||
setSettings(rows);
|
||||
const init: Record<string, string> = {};
|
||||
const vals: Record<string, string> = { default_booking_price: "0" };
|
||||
const dates: Record<string, string> = {};
|
||||
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")))
|
||||
.finally(() => setLoading(false));
|
||||
@@ -43,10 +50,11 @@ export default function AdminParametres() {
|
||||
const handleSave = async (key: string) => {
|
||||
setSaving(key);
|
||||
try {
|
||||
const meta = SETTING_META[key];
|
||||
const meta = SETTING_META[key as SettingKey];
|
||||
const raw = values[key] ?? "";
|
||||
const value = meta?.type === "number" ? parseFloat(raw) : raw;
|
||||
await adminUpdateSetting(key, value);
|
||||
const value = meta?.type === "number" ? parseFloat(raw) || 0 : raw;
|
||||
const saved = await adminUpdateSetting(key, value);
|
||||
if (saved.updated_at) setUpdatedAt((prev) => ({ ...prev, [key]: saved.updated_at }));
|
||||
toast.success(t("admin.settings.saved"));
|
||||
} catch (err) {
|
||||
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 (
|
||||
<div className="space-y-8 max-w-2xl">
|
||||
<div>
|
||||
@@ -67,76 +72,46 @@ export default function AdminParametres() {
|
||||
|
||||
{loading ? (
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t("admin.settings.bookings")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{knownKeys.map((s) => {
|
||||
const meta = SETTING_META[s.key];
|
||||
return (
|
||||
<div key={s.key}>
|
||||
<Label htmlFor={s.key}>{meta.label}</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">{meta.description}</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id={s.key}
|
||||
type={meta.type}
|
||||
value={values[s.key] ?? ""}
|
||||
onChange={(e) => setValues((prev) => ({ ...prev, [s.key]: e.target.value }))}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<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>
|
||||
{s.updated_at && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t("admin.settings.last_updated")} {new Date(s.updated_at).toLocaleDateString(locale)}
|
||||
</p>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t("admin.settings.bookings")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{SETTING_KEYS.map((key) => {
|
||||
const meta = SETTING_META[key];
|
||||
return (
|
||||
<div key={key}>
|
||||
<Label htmlFor={key}>{meta.label}</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">{meta.description}</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id={key}
|
||||
type={meta.type}
|
||||
min={meta.type === "number" ? 0 : undefined}
|
||||
value={values[key] ?? ""}
|
||||
onChange={(e) => setValues((prev) => ({ ...prev, [key]: e.target.value }))}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<Button size="sm" onClick={() => handleSave(key)} disabled={saving === key}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{saving === key ? t("admin.saving") : t("admin.save")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{knownKeys.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t("admin.settings.no_settings")}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
{updatedAt[key] && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t("admin.settings.last_updated")} {new Date(updatedAt[key]).toLocaleDateString(locale)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ type FormState = {
|
||||
category: Product["category"];
|
||||
price: string;
|
||||
original_price: string;
|
||||
stock_quantity: string;
|
||||
description: string;
|
||||
colors: string;
|
||||
lengths: string;
|
||||
@@ -45,6 +46,7 @@ const emptyForm: FormState = {
|
||||
category: "clip-in",
|
||||
price: "",
|
||||
original_price: "",
|
||||
stock_quantity: "0",
|
||||
description: "",
|
||||
colors: "",
|
||||
lengths: "",
|
||||
@@ -86,6 +88,7 @@ export default function AdminProducts() {
|
||||
category: p.category,
|
||||
price: String(p.price),
|
||||
original_price: p.originalPrice ? String(p.originalPrice) : "",
|
||||
stock_quantity: String(p.stockQuantity ?? 0),
|
||||
description: p.description,
|
||||
colors: p.colors.join(", "),
|
||||
lengths: p.lengths.join(", "),
|
||||
@@ -123,6 +126,7 @@ export default function AdminProducts() {
|
||||
category: form.category,
|
||||
price,
|
||||
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),
|
||||
lengths: form.lengths.split(",").map((l) => l.trim()).filter(Boolean),
|
||||
description: form.description,
|
||||
@@ -195,6 +199,7 @@ export default function AdminProducts() {
|
||||
<TableHead>{t("admin.products.col_name")}</TableHead>
|
||||
<TableHead>{t("admin.products.col_category")}</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 className="text-right">{t("admin.actions")}</TableHead>
|
||||
</TableRow>
|
||||
@@ -214,6 +219,9 @@ export default function AdminProducts() {
|
||||
<TableCell className="font-medium">{p.name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{categoryLabels[p.category]}</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">
|
||||
{p.isNew && <Badge variant="secondary">{t("admin.products.badge_new")}</Badge>}
|
||||
{p.isBestseller && <Badge>Bestseller</Badge>}
|
||||
@@ -265,6 +273,10 @@ export default function AdminProducts() {
|
||||
<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")} />
|
||||
</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">
|
||||
<Label>{t("admin.products.image")}</Label>
|
||||
<div className="flex items-start gap-4">
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Suspense, useState, useEffect } from "react";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { categories } from "@/data/products";
|
||||
import type { Product } from "@/data/products";
|
||||
import { listProducts } from "@/lib/api/products";
|
||||
import ProductCard from "@/components/ProductCard";
|
||||
|
||||
export default function Shop() {
|
||||
function ShopContent() {
|
||||
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 [loading, setLoading] = useState(true);
|
||||
|
||||
const setCategory = (cat: string) => {
|
||||
router.push(cat === "all" ? "/boutique" : `/boutique?category=${cat}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
listProducts({
|
||||
@@ -31,7 +38,7 @@ export default function Shop() {
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-2 mb-10">
|
||||
<button
|
||||
onClick={() => setSelectedCategory("all")}
|
||||
onClick={() => setCategory("all")}
|
||||
className={`px-4 py-2 rounded-full text-sm transition-colors ${
|
||||
selectedCategory === "all"
|
||||
? "bg-primary text-primary-foreground"
|
||||
@@ -43,7 +50,7 @@ export default function Shop() {
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
onClick={() => setCategory(cat.id)}
|
||||
className={`px-4 py-2 rounded-full text-sm transition-colors cursor-pointer ${
|
||||
selectedCategory === cat.id
|
||||
? "bg-primary text-primary-foreground"
|
||||
@@ -74,3 +81,11 @@ export default function Shop() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Shop() {
|
||||
return (
|
||||
<Suspense>
|
||||
<ShopContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ export default function Auth() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) router.replace("/");
|
||||
if (user) {
|
||||
router.replace(user.role === "admin" ? "/admin" : "/");
|
||||
}
|
||||
}, [user, router]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@@ -29,12 +31,19 @@ export default function Auth() {
|
||||
setLoading(true);
|
||||
try {
|
||||
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 {
|
||||
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) {
|
||||
const msg = err instanceof ApiError ? err.message : t("auth.error");
|
||||
toast.error(msg);
|
||||
|
||||
@@ -81,6 +81,15 @@ export default function Booking() {
|
||||
}
|
||||
};
|
||||
|
||||
const resetBooking = () => {
|
||||
setConfirmed(false);
|
||||
setSelectedService(null);
|
||||
setSelectedDate(undefined);
|
||||
setSelectedSlot(null);
|
||||
setSlots([]);
|
||||
setPhone(user?.phone ?? "");
|
||||
};
|
||||
|
||||
if (confirmed) {
|
||||
return (
|
||||
<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">
|
||||
{selectedService?.name} — {selectedDate?.toLocaleDateString(locale)} {t("booking.confirmed_at")} {selectedSlot?.start_time.slice(0, 5)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
{t("booking.confirmed_desc")}
|
||||
</p>
|
||||
<Button variant="outline" onClick={resetBooking}>
|
||||
{t("booking.new_booking")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function Header() {
|
||||
const currentPath = usePathname();
|
||||
|
||||
const navLinks = [
|
||||
{ to: "/", label: t("nav.home") },
|
||||
{ to: "/boutique", label: t("nav.shop") },
|
||||
{ to: "/reservation", label: t("nav.booking") },
|
||||
{ to: "/a-propos", label: t("nav.about") },
|
||||
|
||||
@@ -11,7 +11,7 @@ interface AuthContextType {
|
||||
isLoading: boolean;
|
||||
isAdmin: boolean;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
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);
|
||||
setUser(profile);
|
||||
if (profile) setUser(profile);
|
||||
return profile;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ interface LanguageContextType {
|
||||
|
||||
const translations: Record<string, Record<Language, string>> = {
|
||||
// ── 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.booking": { fr: "Réservation", de: "Terminbuchung", en: "Book Appointment" },
|
||||
"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.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.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.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" },
|
||||
@@ -298,6 +301,7 @@ const translations: Record<string, Record<Language, string>> = {
|
||||
"admin.products.col_name": { fr: "Nom", de: "Name", en: "Name" },
|
||||
"admin.products.col_category": { fr: "Catégorie", de: "Kategorie", en: "Category" },
|
||||
"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.badge_new": { fr: "Nouveau", de: "Neu", en: "New" },
|
||||
"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.price": { fr: "Prix (€)", de: "Preis (€)", en: "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.image": { fr: "Image", de: "Bild", en: "Image" },
|
||||
"admin.products.description": { fr: "Description", de: "Beschreibung", en: "Description" },
|
||||
@@ -367,7 +372,9 @@ const translations: Record<string, Record<Language, string>> = {
|
||||
"admin.settings.no_settings": { fr: "Aucun paramètre configuré. Les valeurs par défaut sont utilisées.", de: "Keine Einstellungen konfiguriert. Standardwerte werden verwendet.", en: "No settings configured. Default values are used." },
|
||||
"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.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> = {
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Product {
|
||||
isBestseller?: boolean;
|
||||
rating: number;
|
||||
reviewCount: number;
|
||||
stockQuantity?: number;
|
||||
}
|
||||
|
||||
export const products: Product[] = [
|
||||
|
||||
@@ -25,12 +25,14 @@ export async function register(
|
||||
email: string,
|
||||
password: string,
|
||||
name: string
|
||||
): Promise<UserProfile> {
|
||||
const tokens = await api.post<AuthResponse>("/auth/register", { email, password, name });
|
||||
if ("access_token" in (tokens as object)) {
|
||||
setTokens((tokens as AuthResponse).access_token, (tokens as AuthResponse).refresh_token);
|
||||
): Promise<UserProfile | null> {
|
||||
const res = await api.post<AuthResponse | { message: string }>("/auth/register", { email, password, name });
|
||||
if ("access_token" in res) {
|
||||
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> {
|
||||
|
||||
@@ -40,6 +40,7 @@ function toProduct(p: ApiProduct): Product {
|
||||
isBestseller: p.is_bestseller,
|
||||
rating: p.rating,
|
||||
reviewCount: p.review_count,
|
||||
stockQuantity: p.stock_quantity,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user