diff --git a/app/admin/clients/page.tsx b/app/admin/clients/page.tsx index 813b700..b321ab7 100644 --- a/app/admin/clients/page.tsx +++ b/app/admin/clients/page.tsx @@ -110,7 +110,7 @@ export default function AdminClients() { ) : ( customers.map((c) => ( - {c.full_name ?? "—"} + {c.full_name || "—"} {c.email} {c.phone ?? "—"} {c.orders_count} diff --git a/app/admin/parametres/page.tsx b/app/admin/parametres/page.tsx index 4d92c52..c2812a8 100644 --- a/app/admin/parametres/page.tsx +++ b/app/admin/parametres/page.tsx @@ -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([]); - const [values, setValues] = useState>({}); + const [values, setValues] = useState>({ default_booking_price: "0" }); + const [updatedAt, setUpdatedAt] = useState>({}); const [saving, setSaving] = useState(null); const [loading, setLoading] = useState(true); - const SETTING_META: Record = { + const SETTING_META: Record = { 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 = {}; + const vals: Record = { default_booking_price: "0" }; + const dates: Record = {}; 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 (
@@ -67,76 +72,46 @@ export default function AdminParametres() { {loading ? (
- {Array.from({ length: 3 }).map((_, i) => ( + {Array.from({ length: 2 }).map((_, i) => (
))}
) : ( - <> - - - {t("admin.settings.bookings")} - - - {knownKeys.map((s) => { - const meta = SETTING_META[s.key]; - return ( -
- -

{meta.description}

-
- setValues((prev) => ({ ...prev, [s.key]: e.target.value }))} - className="max-w-[200px]" - /> - -
- {s.updated_at && ( -

- {t("admin.settings.last_updated")} {new Date(s.updated_at).toLocaleDateString(locale)} -

- )} + + + {t("admin.settings.bookings")} + + + {SETTING_KEYS.map((key) => { + const meta = SETTING_META[key]; + return ( +
+ +

{meta.description}

+
+ setValues((prev) => ({ ...prev, [key]: e.target.value }))} + className="max-w-[200px]" + /> +
- ); - })} - {knownKeys.length === 0 && ( -

{t("admin.settings.no_settings")}

- )} - - - - {unknownKeys.length > 0 && ( - - - {t("admin.settings.other")} - - - {unknownKeys.map((s) => ( -
- -
- setValues((prev) => ({ ...prev, [s.key]: e.target.value }))} - /> - -
-
- ))} -
-
- )} - + {updatedAt[key] && ( +

+ {t("admin.settings.last_updated")} {new Date(updatedAt[key]).toLocaleDateString(locale)} +

+ )} +
+ ); + })} +
+
)}
); diff --git a/app/admin/produits/page.tsx b/app/admin/produits/page.tsx index eaef62d..bf0250d 100644 --- a/app/admin/produits/page.tsx +++ b/app/admin/produits/page.tsx @@ -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() { {t("admin.products.col_name")} {t("admin.products.col_category")} {t("admin.products.col_price")} + {t("admin.products.col_stock")} {t("admin.products.col_status")} {t("admin.actions")} @@ -214,6 +219,9 @@ export default function AdminProducts() { {p.name} {categoryLabels[p.category]} {p.price} € + + {p.stockQuantity ?? 0} + {p.isNew && {t("admin.products.badge_new")}} {p.isBestseller && Bestseller} @@ -265,6 +273,10 @@ export default function AdminProducts() { setForm({ ...form, original_price: e.target.value })} placeholder={t("admin.products.optional")} />
+
+ + setForm({ ...form, stock_quantity: e.target.value })} /> +
diff --git a/app/boutique/page.tsx b/app/boutique/page.tsx index 0d2ad95..e930236 100644 --- a/app/boutique/page.tsx +++ b/app/boutique/page.tsx @@ -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([]); 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() {
); } + +export default function Shop() { + return ( + + + + ); +} diff --git a/app/connexion/page.tsx b/app/connexion/page.tsx index 478cc13..80d9814 100644 --- a/app/connexion/page.tsx +++ b/app/connexion/page.tsx @@ -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); diff --git a/app/reservation/page.tsx b/app/reservation/page.tsx index 1fe3b34..a2ba6a9 100644 --- a/app/reservation/page.tsx +++ b/app/reservation/page.tsx @@ -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 (
@@ -92,9 +101,12 @@ export default function Booking() {

{selectedService?.name} — {selectedDate?.toLocaleDateString(locale)} {t("booking.confirmed_at")} {selectedSlot?.start_time.slice(0, 5)}

-

+

{t("booking.confirmed_desc")}

+
); diff --git a/components/Header.tsx b/components/Header.tsx index ff84bfb..8de5b8a 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -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") }, diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx index 1da2cf6..54a4857 100644 --- a/contexts/AuthContext.tsx +++ b/contexts/AuthContext.tsx @@ -11,7 +11,7 @@ interface AuthContextType { isLoading: boolean; isAdmin: boolean; login: (email: string, password: string) => Promise; - register: (email: string, password: string, name: string) => Promise; + register: (email: string, password: string, name: string) => Promise; logout: () => void; } @@ -39,9 +39,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { return profile; }; - const register = async (email: string, password: string, name: string): Promise => { + const register = async (email: string, password: string, name: string): Promise => { const profile = await authApi.register(email, password, name); - setUser(profile); + if (profile) setUser(profile); return profile; }; diff --git a/contexts/LanguageContext.tsx b/contexts/LanguageContext.tsx index e3a28a7..827d0a0 100644 --- a/contexts/LanguageContext.tsx +++ b/contexts/LanguageContext.tsx @@ -13,6 +13,8 @@ interface LanguageContextType { const translations: Record> = { // ── 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> = { "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> = { "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> = { "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> = { "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 = { diff --git a/data/products.ts b/data/products.ts index ded3a1c..228a957 100644 --- a/data/products.ts +++ b/data/products.ts @@ -14,6 +14,7 @@ export interface Product { isBestseller?: boolean; rating: number; reviewCount: number; + stockQuantity?: number; } export const products: Product[] = [ diff --git a/lib/api/auth.ts b/lib/api/auth.ts index d9f62a6..052a3a6 100644 --- a/lib/api/auth.ts +++ b/lib/api/auth.ts @@ -25,12 +25,14 @@ export async function register( email: string, password: string, name: string -): Promise { - const tokens = await api.post("/auth/register", { email, password, name }); - if ("access_token" in (tokens as object)) { - setTokens((tokens as AuthResponse).access_token, (tokens as AuthResponse).refresh_token); +): Promise { + const res = await api.post("/auth/register", { email, password, name }); + if ("access_token" in res) { + setTokens(res.access_token, (res as AuthResponse).refresh_token); + return api.get("/auth/me"); } - return api.get("/auth/me"); + // Email confirmation required — no session yet + return null; } export async function getMe(): Promise { diff --git a/lib/api/products.ts b/lib/api/products.ts index 75b4a5c..4c85a8b 100644 --- a/lib/api/products.ts +++ b/lib/api/products.ts @@ -40,6 +40,7 @@ function toProduct(p: ApiProduct): Product { isBestseller: p.is_bestseller, rating: p.rating, reviewCount: p.review_count, + stockQuantity: p.stock_quantity, }; }