feat(admin): Sprint 5 — Audit log + Settings (gouvernance)
This commit is contained in:
parent
2ad4cbed80
commit
79ddcd23f5
14 changed files with 773 additions and 24 deletions
171
src/app/admin/settings/_components/SettingsForms.tsx
Normal file
171
src/app/admin/settings/_components/SettingsForms.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { FormField, inputCls, selectCls } from "@/components/admin/FormField";
|
||||
import {
|
||||
savePlatformSettingsAction,
|
||||
saveStripeSettingsAction,
|
||||
saveThemeSettingsAction,
|
||||
} from "../actions";
|
||||
|
||||
type Action = (fd: FormData) => Promise<{ ok: false; error: string } | { ok: true } | undefined>;
|
||||
|
||||
function FormWrapper({
|
||||
action,
|
||||
children,
|
||||
submitLabel = "Enregistrer",
|
||||
}: {
|
||||
action: Action;
|
||||
children: React.ReactNode;
|
||||
submitLabel?: string;
|
||||
}) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
function onSubmit(fd: FormData) {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
startTransition(async () => {
|
||||
const res = await action(fd);
|
||||
if (res && res.ok === false) setError(res.error);
|
||||
else if (res && res.ok === true) setSuccess("Enregistré.");
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={onSubmit} className="space-y-4">
|
||||
<fieldset disabled={pending} className="space-y-4">
|
||||
{children}
|
||||
{error ? (
|
||||
<div className="rounded border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700">{error}</div>
|
||||
) : null}
|
||||
{success ? (
|
||||
<div className="rounded border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{success}</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-zinc-900 px-5 py-2 text-sm font-semibold text-white hover:bg-zinc-800 disabled:opacity-50"
|
||||
>
|
||||
{pending ? "Enregistrement…" : submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformForm({
|
||||
initial,
|
||||
}: {
|
||||
initial: { name: string; defaultLang: string; activeLangs: string[]; currency: string; commissionPercent: number };
|
||||
}) {
|
||||
return (
|
||||
<FormWrapper action={savePlatformSettingsAction}>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField label="Nom de la plateforme" required>
|
||||
<input name="name" defaultValue={initial.name} required maxLength={80} className={inputCls} />
|
||||
</FormField>
|
||||
<FormField label="Devise (ISO 4217)" required hint="EUR, USD, BRL…">
|
||||
<input
|
||||
name="currency"
|
||||
defaultValue={initial.currency}
|
||||
required
|
||||
pattern="^[A-Z]{3}$"
|
||||
maxLength={3}
|
||||
className={inputCls + " uppercase"}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Langue par défaut" required hint="Code ISO 639-1 (fr, en, pt…)">
|
||||
<input
|
||||
name="defaultLang"
|
||||
defaultValue={initial.defaultLang}
|
||||
required
|
||||
pattern="^[a-zA-Z]{2}$"
|
||||
maxLength={2}
|
||||
className={inputCls + " lowercase"}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Langues actives" required hint="Séparées par virgule (fr, en, pt).">
|
||||
<input
|
||||
name="activeLangs"
|
||||
defaultValue={initial.activeLangs.join(", ")}
|
||||
required
|
||||
className={inputCls + " lowercase"}
|
||||
placeholder="fr, en"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Commission plateforme (%)" hint="Affiché dans les CGV. 0 = pas de commission.">
|
||||
<input
|
||||
name="commissionPercent"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step="0.01"
|
||||
defaultValue={initial.commissionPercent.toString()}
|
||||
className={inputCls}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeForm({ initial }: { initial: { active: string } }) {
|
||||
return (
|
||||
<FormWrapper action={saveThemeSettingsAction}>
|
||||
<FormField label="Thème actif" hint="Détermine la skin du site public.">
|
||||
<select name="active" defaultValue={initial.active} className={selectCls}>
|
||||
<option value="default">default — sobre (admin-like)</option>
|
||||
<option value="theme-aquarelle">theme-aquarelle — carnet naturaliste XIXᵉ</option>
|
||||
<option value="theme-guyane">theme-guyane — palette tropicale</option>
|
||||
</select>
|
||||
</FormField>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export function StripeForm({
|
||||
initial,
|
||||
}: {
|
||||
initial: { currency: string; commissionMode: string; perBookingFeePercent: number };
|
||||
}) {
|
||||
return (
|
||||
<FormWrapper action={saveStripeSettingsAction}>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField label="Devise Stripe" required hint="Doit correspondre à la devise plateforme.">
|
||||
<input
|
||||
name="currency"
|
||||
defaultValue={initial.currency}
|
||||
required
|
||||
pattern="^[A-Z]{3}$"
|
||||
maxLength={3}
|
||||
className={inputCls + " uppercase"}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Modèle économique" required>
|
||||
<select name="commissionMode" defaultValue={initial.commissionMode} className={selectCls}>
|
||||
<option value="none">Aucune monétisation (preview)</option>
|
||||
<option value="owner-subscription">Abonnement loueur (revenu plateforme)</option>
|
||||
<option value="per-booking">Commission par réservation</option>
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Commission par réservation (%)"
|
||||
hint="Utilisé uniquement si modèle = par réservation."
|
||||
>
|
||||
<input
|
||||
name="perBookingFeePercent"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step="0.01"
|
||||
defaultValue={initial.perBookingFeePercent.toString()}
|
||||
className={inputCls}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue