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

@@ -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>
);