132 lines
6.2 KiB
TypeScript
132 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useTransition } from "react";
|
|
import { FormField, inputCls, selectCls, textareaCls } from "@/components/admin/FormField";
|
|
import { RENTAL_CATEGORY_LABEL, RENTAL_CATEGORIES } from "@/lib/rental-category-labels";
|
|
|
|
type Props = {
|
|
providers: { id: string; name: string; isSystemD: boolean }[];
|
|
initial?: {
|
|
providerId?: string;
|
|
category?: string;
|
|
name?: string;
|
|
description?: string | null;
|
|
imageUrl?: string | null;
|
|
pricePerDay?: string | number;
|
|
pricePerWeek?: string | number | null;
|
|
deposit?: string | number;
|
|
totalQty?: number;
|
|
withMotor?: boolean;
|
|
fuelIncluded?: boolean;
|
|
requiresLicense?: boolean;
|
|
active?: boolean;
|
|
};
|
|
action: (fd: FormData) => Promise<{ ok: false; error: string } | { ok: true } | undefined>;
|
|
submitLabel?: string;
|
|
};
|
|
|
|
export function ItemForm({ providers, initial = {}, action, submitLabel = "Enregistrer" }: Props) {
|
|
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">
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<FormField label="Prestataire" required>
|
|
<select name="providerId" defaultValue={initial.providerId ?? ""} required className={selectCls}>
|
|
<option value="" disabled>— sélectionner —</option>
|
|
{providers.map((p) => (
|
|
<option key={p.id} value={p.id}>
|
|
{p.name}{p.isSystemD ? " (System D)" : ""}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Catégorie" required>
|
|
<select name="category" defaultValue={initial.category ?? ""} required className={selectCls}>
|
|
<option value="" disabled>— sélectionner —</option>
|
|
{RENTAL_CATEGORIES.map((c) => (
|
|
<option key={c} value={c}>{RENTAL_CATEGORY_LABEL[c]}</option>
|
|
))}
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Nom de l'item" required className="sm:col-span-2">
|
|
<input name="name" defaultValue={initial.name ?? ""} required maxLength={200} className={inputCls} placeholder="ex. Hamac coton large, Pirogue 5m avec moteur 15CV" />
|
|
</FormField>
|
|
<FormField label="Description" className="sm:col-span-2">
|
|
<textarea name="description" rows={3} defaultValue={initial.description ?? ""} maxLength={5000} className={textareaCls} />
|
|
</FormField>
|
|
<FormField label="URL image" hint="Optionnel, URL publique vers photo MinIO.">
|
|
<input name="imageUrl" type="url" defaultValue={initial.imageUrl ?? ""} maxLength={500} className={inputCls} />
|
|
</FormField>
|
|
<FormField label="Stock total (qté)" required>
|
|
<input name="totalQty" type="number" min={1} max={1000} defaultValue={initial.totalQty?.toString() ?? "1"} required className={inputCls} />
|
|
</FormField>
|
|
<FormField label="Prix / jour (€)" required>
|
|
<input name="pricePerDay" type="number" min={0} step="0.5" defaultValue={initial.pricePerDay?.toString() ?? ""} required className={inputCls} />
|
|
</FormField>
|
|
<FormField label="Prix / semaine (€)" hint="Optionnel — tarif dégressif sur 7+ jours.">
|
|
<input name="pricePerWeek" type="number" min={0} step="0.5" defaultValue={initial.pricePerWeek?.toString() ?? ""} className={inputCls} />
|
|
</FormField>
|
|
<FormField label="Caution (€)" hint="Dépôt de garantie (bloqué pendant la location).">
|
|
<input name="deposit" type="number" min={0} step="1" defaultValue={initial.deposit?.toString() ?? "0"} className={inputCls} />
|
|
</FormField>
|
|
<FormField label="Statut">
|
|
<label className="flex items-center gap-2 px-1 py-2 text-sm">
|
|
<input type="checkbox" name="active" defaultChecked={initial.active ?? true} className="h-4 w-4 rounded border-zinc-300" />
|
|
Actif (visible au catalogue)
|
|
</label>
|
|
</FormField>
|
|
</div>
|
|
|
|
<fieldset className="rounded-lg border border-zinc-200 bg-zinc-50 p-3">
|
|
<legend className="px-1 text-xs font-semibold uppercase tracking-wider text-zinc-500">
|
|
Spécifications navigation
|
|
</legend>
|
|
<div className="flex flex-wrap gap-4 pt-1 text-sm">
|
|
<label className="flex items-center gap-2">
|
|
<input type="checkbox" name="withMotor" defaultChecked={initial.withMotor ?? false} className="h-4 w-4 rounded border-zinc-300" />
|
|
Avec moteur
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input type="checkbox" name="fuelIncluded" defaultChecked={initial.fuelIncluded ?? false} className="h-4 w-4 rounded border-zinc-300" />
|
|
Essence incluse
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input type="checkbox" name="requiresLicense" defaultChecked={initial.requiresLicense ?? false} className="h-4 w-4 rounded border-zinc-300" />
|
|
Permis bateau requis
|
|
</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
{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>
|
|
);
|
|
}
|