ldc-ovoteam/resources/ts/back/Components/Inputs/NutritionalValuesInput.tsx

319 lines
9.5 KiB
TypeScript

import { PlusIcon, Trash2Icon } from "lucide-react";
import { useMemo, useState } from "react";
import { NutritionnalValues } from "@/back/types/products";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export type NutrientKey = keyof Omit<NutritionnalValues, "refWeightG">;
export interface NutrientDefinition {
key: NutrientKey;
label: string;
defaultUnit: "g" | "kcal" | "kj";
unitLabel: string;
}
export const NUTRIENT_DEFINITIONS: readonly NutrientDefinition[] = [
{
key: "kcal",
label: "Énergie (Kilocalorie)",
defaultUnit: "kcal",
unitLabel: "Kcal",
},
{
key: "kj",
label: "Énergie (Kilojoule)",
defaultUnit: "kj",
unitLabel: "Kj",
},
{
key: "proteinG",
label: "Protéines",
defaultUnit: "g",
unitLabel: "g",
},
{
key: "carbG",
label: "Glucides",
defaultUnit: "g",
unitLabel: "g",
},
{
key: "sugarG",
label: "Sucres",
defaultUnit: "g",
unitLabel: "g",
},
{
key: "fatG",
label: "Matières grasses",
defaultUnit: "g",
unitLabel: "g",
},
{
key: "satFatG",
label: "Acides gras saturés",
defaultUnit: "g",
unitLabel: "g",
},
{
key: "fiberG",
label: "Fibres alimentaires",
defaultUnit: "g",
unitLabel: "g",
},
{
key: "saltG",
label: "Sel",
defaultUnit: "g",
unitLabel: "g",
},
] as const;
interface NutritionalValuesInputProps {
value: NutritionnalValues | null | undefined;
setData: (key: string, value: unknown) => void;
parentErrors?: Record<string, string> | null;
}
export default function NutritionalValuesInput({
value,
setData,
parentErrors,
}: Readonly<NutritionalValuesInputProps>) {
const initialKeys = useMemo<NutrientKey[]>(() => {
if (!value) return [];
return NUTRIENT_DEFINITIONS.filter(
(def) =>
value[def.key] !== null &&
value[def.key] !== undefined &&
(value[def.key]?.value !== "" || value[def.key]?.value !== undefined),
).map((def) => def.key);
}, [value]);
const [activeKeys, setActiveKeys] = useState<NutrientKey[]>(initialKeys);
const refWeight = value?.refWeightG?.value ?? "100";
const refWeightUnit = value?.refWeightG?.unit ?? "g";
const handleRefWeightChange = (newVal: string) => {
setData("nutritional_values", {
...value,
refWeightG: {
value: newVal,
unit: refWeightUnit,
},
});
};
const handleAddLine = () => {
const nextDef = NUTRIENT_DEFINITIONS.find(
(def) => !activeKeys.includes(def.key),
);
if (!nextDef) return;
setActiveKeys((prev) => [...prev, nextDef.key]);
setData("nutritional_values", {
...value,
refWeightG: value?.refWeightG ?? { value: refWeight, unit: "g" },
[nextDef.key]: {
value: "",
unit: nextDef.defaultUnit,
},
});
};
const handleRemoveLine = (keyToRemove: NutrientKey) => {
setActiveKeys((prev) => prev.filter((k) => k !== keyToRemove));
const updatedValues = { ...value };
delete updatedValues[keyToRemove];
setData("nutritional_values", updatedValues);
};
const handleNutrientChange = (oldKey: NutrientKey, newKeyString: string) => {
const newKey = newKeyString as NutrientKey;
if (oldKey === newKey) return;
const newDef = NUTRIENT_DEFINITIONS.find((def) => def.key === newKey);
if (!newDef) return;
const prevNutrientVal = value?.[oldKey]?.value ?? "";
setActiveKeys((prev) => prev.map((k) => (k === oldKey ? newKey : k)));
const updatedValues = { ...value };
delete updatedValues[oldKey];
updatedValues[newKey] = {
value: prevNutrientVal,
unit: newDef.defaultUnit,
};
setData("nutritional_values", updatedValues);
};
const handleValueChange = (key: NutrientKey, val: string) => {
const def = NUTRIENT_DEFINITIONS.find((d) => d.key === key);
if (!def) return;
const currentUnit = value?.[key]?.unit ?? def.defaultUnit;
setData("nutritional_values", {
...(value ?? {}),
refWeightG: value?.refWeightG ?? { value: refWeight, unit: "g" },
[key]: {
value: val,
unit: currentUnit,
},
});
};
const hasAvailableNutrients = activeKeys.length < NUTRIENT_DEFINITIONS.length;
return (
<fieldset className="border-2 border-slate-200 p-4 rounded-lg space-y-4">
<legend>
<Label className="font-bold text-base">Valeurs nutritionnelles</Label>
</legend>
{/* Poids de référence */}
<div className="max-w-xs space-y-1">
<Label htmlFor="nut_ref_weight" className="text-sm font-semibold">
Poids de référence
</Label>
<div className="flex items-center gap-2">
<Input
id="nut_ref_weight"
type="text"
value={refWeight}
placeholder="100"
onChange={(e) => handleRefWeightChange(e.target.value)}
className="w-32"
/>
<span className="text-sm font-medium text-slate-600">
{refWeightUnit}
</span>
</div>
</div>
{/* Gestion des lignes */}
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between border-b pb-2">
<h4 className="font-semibold text-sm text-slate-700">
Valeurs de référence
</h4>
{hasAvailableNutrients && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddLine}
className="flex items-center gap-1.5"
>
<PlusIcon className="w-4 h-4" />
<span>Ajouter une ligne</span>
</Button>
)}
</div>
{activeKeys.length === 0 ? (
<p className="text-sm text-slate-500 italic py-2">
Aucune ligne nutritionnelle renseignée. Cliquez sur &quot;Ajouter
une ligne&quot; pour en ajouter une.
</p>
) : (
<div className="space-y-3">
{activeKeys.map((key) => {
const def = NUTRIENT_DEFINITIONS.find((d) => d.key === key);
if (!def) return null;
const nutrientVal = value?.[key]?.value ?? "";
const unitLabel = def.unitLabel;
return (
<div
key={key}
className="flex flex-col sm:flex-row items-start sm:items-center gap-3 bg-slate-50 p-3 rounded-md border border-slate-200"
>
{/* Select Libellé valeur / Unité */}
<div className="w-full sm:w-72">
<Label className="text-xs text-slate-500 sm:hidden mb-1 block">
Libellé valeur / Unité
</Label>
<Select
value={key}
onValueChange={(newVal) =>
handleNutrientChange(key, newVal)
}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="Sélectionner..." />
</SelectTrigger>
<SelectContent>
{NUTRIENT_DEFINITIONS.map((option) => (
<SelectItem
key={option.key}
value={option.key}
disabled={
activeKeys.includes(option.key) &&
option.key !== key
}
>
{option.label} / {option.unitLabel}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Champ Valeur */}
<div className="flex items-center gap-2 flex-1 w-full sm:w-auto">
<Label className="text-xs text-slate-500 sm:hidden">
Valeur
</Label>
<div className="relative flex-1">
<Input
type="text"
value={nutrientVal}
placeholder="Ex : <0,5 ou 14"
onChange={(e) => handleValueChange(key, e.target.value)}
className="bg-white pr-14"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-semibold text-slate-400 select-none">
{unitLabel}
</span>
</div>
</div>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveLine(key)}
className="text-red-500 hover:text-red-700 hover:bg-red-50 self-end sm:self-center"
title="Supprimer cette ligne"
>
<Trash2Icon className="w-4 h-4" />
</Button>
</div>
);
})}
</div>
)}
</div>
{parentErrors?.nutritional_values && (
<p className="text-sm text-red-500 mt-1">
{parentErrors.nutritional_values}
</p>
)}
</fieldset>
);
}