319 lines
9.5 KiB
TypeScript
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 "Ajouter
|
|
une ligne" 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>
|
|
);
|
|
}
|