refactor: add missing nutritional values on products
This commit is contained in:
parent
325c7a7fd6
commit
597e1c720a
14 changed files with 733 additions and 374 deletions
|
|
@ -4,13 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Casts;
|
||||
|
||||
use Domain\Models\ValueObjects\Products\NutritionalValue;
|
||||
use Domain\Models\ValueObjects\Products\NutritionalValues;
|
||||
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/** @implements CastsAttributes<NutritionalValues|null,NutritionalValues|null> */
|
||||
/** @implements CastsAttributes<NutritionalValues|null, NutritionalValues|array<string, mixed>|null> */
|
||||
class NutritionalValuesCast implements CastsAttributes
|
||||
{
|
||||
/** @param array<string, mixed> $attributes */
|
||||
|
|
@ -23,43 +24,10 @@ class NutritionalValuesCast implements CastsAttributes
|
|||
$data = is_string($value) ? json_decode($value, true) : $value;
|
||||
|
||||
if (! is_array($data)) {
|
||||
throw new \UnexpectedValueException(
|
||||
'Expected data, should have been an array'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = array_filter($data, fn ($key) => is_string($key), \ARRAY_FILTER_USE_KEY);
|
||||
|
||||
return new NutritionalValues(
|
||||
refWeightG: $this->getFloatOrNull($data, 'nv_ref_weight_g'),
|
||||
kj: $this->getFloatOrNull($data, 'nv_kj'),
|
||||
kcal: $this->getFloatOrNull($data, 'nv_kcal'),
|
||||
fatG: $this->getFloatOrNull($data, 'nv_fat_g'),
|
||||
satFatG: $this->getFloatOrNull($data, 'nv_sat_fat_g'),
|
||||
carbG: $this->getFloatOrNull($data, 'nv_carb_g'),
|
||||
sugarG: $this->getFloatOrNull($data, 'nv_sugar_g'),
|
||||
fiberG: $this->getFloatOrNull($data, 'nv_fiber_g'),
|
||||
proteinG: $this->getFloatOrNull($data, 'nv_protein_g'),
|
||||
saltG: $this->getFloatOrNull($data, 'nv_salt_g'),
|
||||
servingSizeG: $this->getFloatOrNull($data, 'nv_serving_size_g'),
|
||||
servingKj: $this->getFloatOrNull($data, 'nv_serving_kj'),
|
||||
servingKcal: $this->getFloatOrNull($data, 'nv_serving_kcal'),
|
||||
servingFatG: $this->getFloatOrNull($data, 'nv_serving_fat_g'),
|
||||
servingSatFatG: $this->getFloatOrNull($data, 'nv_serving_sat_fat_g'),
|
||||
servingCarbG: $this->getFloatOrNull($data, 'nv_serving_carb_g'),
|
||||
servingSugarG: $this->getFloatOrNull($data, 'nv_serving_sugar_g'),
|
||||
servingFiberG: $this->getFloatOrNull($data, 'nv_serving_fiber_g'),
|
||||
servingProteinG: $this->getFloatOrNull($data, 'nv_serving_protein_g'),
|
||||
servingSaltG: $this->getFloatOrNull($data, 'nv_serving_salt_g'),
|
||||
dailyEnergy: $this->getFloatOrNull($data, 'nv_daily_energy'),
|
||||
dailyFat: $this->getFloatOrNull($data, 'nv_daily_fat'),
|
||||
dailySatFat: $this->getFloatOrNull($data, 'nv_daily_sat_fat'),
|
||||
dailyCarb: $this->getFloatOrNull($data, 'nv_daily_carb'),
|
||||
dailySugar: $this->getFloatOrNull($data, 'nv_daily_sugar'),
|
||||
dailyFiber: $this->getFloatOrNull($data, 'nv_daily_fiber'),
|
||||
dailyProtein: $this->getFloatOrNull($data, 'nv_daily_protein'),
|
||||
dailySalt: $this->getFloatOrNull($data, 'nv_daily_salt'),
|
||||
);
|
||||
return $this->fromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -72,6 +40,14 @@ class NutritionalValuesCast implements CastsAttributes
|
|||
return [$key => null];
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = json_decode($value, true);
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = $this->fromArray($value);
|
||||
}
|
||||
|
||||
if (! $value instanceof NutritionalValues) {
|
||||
throw new InvalidArgumentException('The given value is not a NutritionalValues instance.');
|
||||
}
|
||||
|
|
@ -80,24 +56,63 @@ class NutritionalValuesCast implements CastsAttributes
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @param array<array-key, mixed> $data
|
||||
*/
|
||||
private function getFloatOrNull(array $attributes, string $key): ?float
|
||||
private function fromArray(array $data): ?NutritionalValues
|
||||
{
|
||||
$value = Arr::get($attributes, $key);
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (float) $value;
|
||||
$refWeightData = $data['refWeightG'] ?? null;
|
||||
if (! is_array($refWeightData) || ! isset($refWeightData['value'], $refWeightData['unit'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw_if(
|
||||
$value !== null && $value !== '',
|
||||
InvalidArgumentException::class,
|
||||
"Expected `{$key}` to be numeric or null"
|
||||
);
|
||||
$rawVal = $refWeightData['value'];
|
||||
$rawUnit = $refWeightData['unit'];
|
||||
|
||||
return null;
|
||||
if ((! is_string($rawVal) && ! is_numeric($rawVal)) || ! is_string($rawUnit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$refWeightUnit = NutritionnalUnit::tryFrom($rawUnit);
|
||||
if ($refWeightUnit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parseItem = function (mixed $item): ?NutritionalValue {
|
||||
if (! is_array($item) || ! isset($item['value'], $item['unit'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rawItemVal = $item['value'];
|
||||
$rawItemUnit = $item['unit'];
|
||||
|
||||
if ((! is_string($rawItemVal) && ! is_numeric($rawItemVal)) || ! is_string($rawItemUnit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim((string) $rawItemVal);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$unit = NutritionnalUnit::tryFrom($rawItemUnit);
|
||||
if ($unit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new NutritionalValue($value, $unit);
|
||||
};
|
||||
|
||||
return new NutritionalValues(
|
||||
refWeightG: new NutritionalValue((string) $rawVal, $refWeightUnit),
|
||||
kj: $parseItem($data['kj'] ?? null),
|
||||
kcal: $parseItem($data['kcal'] ?? null),
|
||||
fatG: $parseItem($data['fatG'] ?? null),
|
||||
satFatG: $parseItem($data['satFatG'] ?? null),
|
||||
carbG: $parseItem($data['carbG'] ?? null),
|
||||
sugarG: $parseItem($data['sugarG'] ?? null),
|
||||
fiberG: $parseItem($data['fiberG'] ?? null),
|
||||
proteinG: $parseItem($data['proteinG'] ?? null),
|
||||
saltG: $parseItem($data['saltG'] ?? null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use App\Http\Resources\CommonResource;
|
|||
use App\Models\Product;
|
||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
||||
use Domain\Models\ValueObjects\Products\Business;
|
||||
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
||||
use Domain\Models\ValueObjects\Products\Technology;
|
||||
|
|
@ -85,12 +86,13 @@ class ProductController extends BackendController
|
|||
$this->authorize('create', Product::class);
|
||||
|
||||
return $this->inertiaRender('back/Pages/Products/Create', [
|
||||
'data' => CommonResource::make($product),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumTechnology' => Technology::toArray(),
|
||||
'enumBreedingMethod' => BreedingMethod::toArray(),
|
||||
'enumBusiness' => Business::toArray(),
|
||||
'enumProductFormat' => ProductFormat::toArray(),
|
||||
'data' => CommonResource::make($product),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumTechnology' => Technology::toArray(),
|
||||
'enumBreedingMethod' => BreedingMethod::toArray(),
|
||||
'enumBusiness' => Business::toArray(),
|
||||
'enumProductFormat' => ProductFormat::toArray(),
|
||||
'enumNutritionnalUnit' => NutritionnalUnit::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -123,12 +125,13 @@ class ProductController extends BackendController
|
|||
$this->authorize('update', $product);
|
||||
|
||||
return $this->inertiaRender('back/Pages/Products/Edit', [
|
||||
'data' => CommonResource::make($product),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumTechnology' => Technology::toArray(),
|
||||
'enumBreedingMethod' => BreedingMethod::toArray(),
|
||||
'enumBusiness' => Business::toArray(),
|
||||
'enumProductFormat' => ProductFormat::toArray(),
|
||||
'data' => CommonResource::make($product),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumTechnology' => Technology::toArray(),
|
||||
'enumBreedingMethod' => BreedingMethod::toArray(),
|
||||
'enumBusiness' => Business::toArray(),
|
||||
'enumProductFormat' => ProductFormat::toArray(),
|
||||
'enumNutritionnalUnit' => NutritionnalUnit::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace App\Http\Requests\Back\Products;
|
|||
use App\Services\AdminAuthService;
|
||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
||||
use Domain\Models\ValueObjects\Products\Business;
|
||||
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
||||
use Domain\Models\ValueObjects\Products\Technology;
|
||||
|
|
@ -79,6 +80,9 @@ class StoreProductRequest extends FormRequest
|
|||
->max(self::MAX_FILE_SIZE_KB)
|
||||
->types('pdf'),
|
||||
],
|
||||
'nutritional_values' => ['nullable', 'array'],
|
||||
'nutritional_values.*.value' => ['required_with:nutritional_values.*.unit', 'string', 'min:1', 'max:50'],
|
||||
'nutritional_values.*.unit' => ['required_with:nutritional_values.*.value', new Enum(NutritionnalUnit::class)],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -90,28 +94,40 @@ class StoreProductRequest extends FormRequest
|
|||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => trans('validation.attributes.name'),
|
||||
'seo_title' => trans('validation.attributes.seo_title'),
|
||||
'seo_description' => trans('validation.attributes.seo_description'),
|
||||
'category' => trans('validation.attributes.category'),
|
||||
'technology' => trans('validation.attributes.technology'),
|
||||
'is_egalim' => trans('validation.attributes.is_egalim'),
|
||||
'breeding_methods' => trans('validation.attributes.breeding_methods'),
|
||||
'breeding_methods.*' => trans('validation.attributes.breeding_methods'),
|
||||
'businesses' => trans('validation.attributes.businesses'),
|
||||
'businesses.*' => trans('validation.attributes.businesses'),
|
||||
'formats' => trans('validation.attributes.formats'),
|
||||
'formats.*' => trans('validation.attributes.formats'),
|
||||
'description' => trans('validation.attributes.description'),
|
||||
'details_and_advantages' => trans('validation.attributes.details_and_advantages'),
|
||||
'handling_advices' => trans('validation.attributes.handling_advices'),
|
||||
'ingredients' => trans('validation.attributes.ingredients'),
|
||||
'conservation_before_opening' => trans('validation.attributes.conservation_before_opening'),
|
||||
'conservation_after_opening' => trans('validation.attributes.conservation_after_opening'),
|
||||
'picture' => trans('validation.attributes.picture'),
|
||||
'picture_alt' => trans('validation.attributes.picture_alt'),
|
||||
'picture_title' => trans('validation.attributes.picture_title'),
|
||||
'pdf' => trans('validation.attributes.pdf'),
|
||||
'name' => trans('validation.attributes.name'),
|
||||
'seo_title' => trans('validation.attributes.seo_title'),
|
||||
'seo_description' => trans('validation.attributes.seo_description'),
|
||||
'category' => trans('validation.attributes.category'),
|
||||
'technology' => trans('validation.attributes.technology'),
|
||||
'is_egalim' => trans('validation.attributes.is_egalim'),
|
||||
'breeding_methods' => trans('validation.attributes.breeding_methods'),
|
||||
'breeding_methods.*' => trans('validation.attributes.breeding_methods'),
|
||||
'businesses' => trans('validation.attributes.businesses'),
|
||||
'businesses.*' => trans('validation.attributes.businesses'),
|
||||
'formats' => trans('validation.attributes.formats'),
|
||||
'formats.*' => trans('validation.attributes.formats'),
|
||||
'description' => trans('validation.attributes.description'),
|
||||
'details_and_advantages' => trans('validation.attributes.details_and_advantages'),
|
||||
'handling_advices' => trans('validation.attributes.handling_advices'),
|
||||
'ingredients' => trans('validation.attributes.ingredients'),
|
||||
'conservation_before_opening' => trans('validation.attributes.conservation_before_opening'),
|
||||
'conservation_after_opening' => trans('validation.attributes.conservation_after_opening'),
|
||||
'picture' => trans('validation.attributes.picture'),
|
||||
'picture_alt' => trans('validation.attributes.picture_alt'),
|
||||
'picture_title' => trans('validation.attributes.picture_title'),
|
||||
'pdf' => trans('validation.attributes.pdf'),
|
||||
'nutritional_values' => trans('validation.attributes.nutritional_values'),
|
||||
'nutritional_values.refWeightG.value' => trans('validation.attributes.nutritional_refWeightG'),
|
||||
'nutritional_values.kj.value' => trans('validation.attributes.nutritional_kj'),
|
||||
'nutritional_values.kcal.value' => trans('validation.attributes.nutritional_kcal'),
|
||||
'nutritional_values.fatG.value' => trans('validation.attributes.nutritional_fatG'),
|
||||
'nutritional_values.satFatG.value' => trans('validation.attributes.nutritional_satFatG'),
|
||||
'nutritional_values.carbG.value' => trans('validation.attributes.nutritional_carbG'),
|
||||
'nutritional_values.sugarG.value' => trans('validation.attributes.nutritional_sugarG'),
|
||||
'nutritional_values.fiberG.value' => trans('validation.attributes.nutritional_fiberG'),
|
||||
'nutritional_values.proteinG.value' => trans('validation.attributes.nutritional_proteinG'),
|
||||
'nutritional_values.saltG.value' => trans('validation.attributes.nutritional_saltG'),
|
||||
'nutritional_values.*.unit' => trans('validation.attributes.nutritional_values_unit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ namespace Database\Seeders\Mandatory;
|
|||
use App\Models\Product;
|
||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
||||
use Domain\Models\ValueObjects\Products\Business;
|
||||
use Domain\Models\ValueObjects\Products\NutritionalValue;
|
||||
use Domain\Models\ValueObjects\Products\NutritionalValues;
|
||||
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
||||
use Domain\Models\ValueObjects\Products\Technology;
|
||||
|
|
@ -349,29 +351,48 @@ class ImportProductsCsvSeeder extends Seeder
|
|||
*/
|
||||
private function extractNutritionalValues(array $paddedRow): ?NutritionalValues
|
||||
{
|
||||
$nvRefWeight = $this->toFloat($this->strOf($paddedRow, self::COL_NV_REF_WEIGHT));
|
||||
$nvKcal = $this->toFloat($this->strOf($paddedRow, self::COL_NV_KCAL));
|
||||
$nvKj = $this->toFloat($this->strOf($paddedRow, self::COL_NV_KJ));
|
||||
$nvProtein = $this->toFloat($this->strOf($paddedRow, self::COL_NV_PROTEIN));
|
||||
$nvCarb = $this->toFloat($this->strOf($paddedRow, self::COL_NV_CARB));
|
||||
$nvSugar = $this->toFloat($this->strOf($paddedRow, self::COL_NV_SUGAR));
|
||||
$nvFat = $this->toFloat($this->strOf($paddedRow, self::COL_NV_FAT));
|
||||
$nvSatFat = $this->toFloat($this->strOf($paddedRow, self::COL_NV_SAT_FAT));
|
||||
$nvFiber = $this->toFloat($this->strOf($paddedRow, self::COL_NV_FIBER));
|
||||
$nvSalt = $this->toFloat($this->strOf($paddedRow, self::COL_NV_SALT));
|
||||
$nvRefWeight = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_REF_WEIGHT),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
$nvKcal = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_KCAL),
|
||||
NutritionnalUnit::KCAL
|
||||
);
|
||||
$nvKj = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_KJ),
|
||||
NutritionnalUnit::KJ
|
||||
);
|
||||
$nvProtein = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_PROTEIN),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
$nvCarb = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_CARB),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
$nvSugar = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_SUGAR),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
$nvFat = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_FAT),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
$nvSatFat = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_SAT_FAT),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
$nvFiber = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_FIBER),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
$nvSalt = $this->toNutritionalValue(
|
||||
$this->strOf($paddedRow, self::COL_NV_SALT),
|
||||
NutritionnalUnit::GRAM
|
||||
);
|
||||
|
||||
$hasNutritionalData = ! empty(array_filter([
|
||||
$nvRefWeight,
|
||||
$nvKcal,
|
||||
$nvKj,
|
||||
$nvProtein,
|
||||
$nvCarb,
|
||||
$nvSugar,
|
||||
$nvFat,
|
||||
$nvSatFat,
|
||||
$nvFiber,
|
||||
$nvSalt,
|
||||
], fn ($val) => $val !== null));
|
||||
$hasNutritionalData = $nvRefWeight !== null;
|
||||
|
||||
return $hasNutritionalData ? new NutritionalValues(
|
||||
refWeightG: $nvRefWeight,
|
||||
|
|
@ -500,19 +521,25 @@ class ImportProductsCsvSeeder extends Seeder
|
|||
}
|
||||
|
||||
/**
|
||||
* Nettoie les valeurs numériques françaises (12,5 -> 12.5) et gère les cas comme "<0,5"
|
||||
* Normalise et convertit une valeur nutritionnelle en objet NutritionalValue.
|
||||
*/
|
||||
private function toFloat(Stringable $value): ?float
|
||||
private function toNutritionalValue(Stringable $value, NutritionnalUnit $unit): ?NutritionalValue
|
||||
{
|
||||
$value = $value->trim();
|
||||
if ($value->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $value->replace(['<', '>'], '');
|
||||
$value = $value->replace(',', '.');
|
||||
$numericValue = $value
|
||||
->replace('.', ',')
|
||||
->replace(['<', '>'], '')
|
||||
->toFloat();
|
||||
$prefix = $value->startsWith(['<', '>']) ? $value->take(1) : '';
|
||||
|
||||
return $value->toFloat();
|
||||
return new NutritionalValue(
|
||||
"{$prefix} " . number_format($numericValue, 2, ',', ''),
|
||||
$unit
|
||||
);
|
||||
}
|
||||
|
||||
private function applyLabelMappings(Stringable $label): Stringable
|
||||
|
|
|
|||
42
domain/Models/ValueObjects/Products/NutritionalValue.php
Normal file
42
domain/Models/ValueObjects/Products/NutritionalValue.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Products;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, string>
|
||||
*/
|
||||
final readonly class NutritionalValue implements Arrayable, JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
public NutritionnalUnit $unit
|
||||
) {
|
||||
if (empty(mb_trim($value))) {
|
||||
throw new \DomainException('Value cannot be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{value: string, unit: string}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'value' => $this->value,
|
||||
'unit' => $this->unit->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{value: string, unit: string}
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,116 +4,54 @@ declare(strict_types=1);
|
|||
|
||||
namespace Domain\Models\ValueObjects\Products;
|
||||
|
||||
final readonly class NutritionalValues
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, array{value: string, unit: string}|null>
|
||||
*/
|
||||
final readonly class NutritionalValues implements Arrayable, JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
// * Base
|
||||
public ?float $refWeightG = null,
|
||||
public NutritionalValue $refWeightG,
|
||||
// * Pour 100g ou par référence
|
||||
public ?float $kj = null,
|
||||
public ?float $kcal = null,
|
||||
public ?float $fatG = null,
|
||||
public ?float $satFatG = null,
|
||||
public ?float $carbG = null,
|
||||
public ?float $sugarG = null,
|
||||
public ?float $fiberG = null,
|
||||
public ?float $proteinG = null,
|
||||
public ?float $saltG = null,
|
||||
// * Par portion
|
||||
public ?float $servingSizeG = null,
|
||||
public ?float $servingKj = null,
|
||||
public ?float $servingKcal = null,
|
||||
public ?float $servingFatG = null,
|
||||
public ?float $servingSatFatG = null,
|
||||
public ?float $servingCarbG = null,
|
||||
public ?float $servingSugarG = null,
|
||||
public ?float $servingFiberG = null,
|
||||
public ?float $servingProteinG = null,
|
||||
public ?float $servingSaltG = null,
|
||||
// * Apports journaliers (% AR)
|
||||
public ?float $dailyEnergy = null,
|
||||
public ?float $dailyFat = null,
|
||||
public ?float $dailySatFat = null,
|
||||
public ?float $dailyCarb = null,
|
||||
public ?float $dailySugar = null,
|
||||
public ?float $dailyFiber = null,
|
||||
public ?float $dailyProtein = null,
|
||||
public ?float $dailySalt = null,
|
||||
public ?NutritionalValue $kj = null,
|
||||
public ?NutritionalValue $kcal = null,
|
||||
public ?NutritionalValue $fatG = null,
|
||||
public ?NutritionalValue $satFatG = null,
|
||||
public ?NutritionalValue $carbG = null,
|
||||
public ?NutritionalValue $sugarG = null,
|
||||
public ?NutritionalValue $fiberG = null,
|
||||
public ?NutritionalValue $proteinG = null,
|
||||
public ?NutritionalValue $saltG = null,
|
||||
) {
|
||||
$values = [
|
||||
$refWeightG,
|
||||
$kj,
|
||||
$kcal,
|
||||
$fatG,
|
||||
$satFatG,
|
||||
$carbG,
|
||||
$sugarG,
|
||||
$fiberG,
|
||||
$proteinG,
|
||||
$saltG,
|
||||
$servingSizeG,
|
||||
$servingKj,
|
||||
$servingKcal,
|
||||
$servingFatG,
|
||||
$servingSatFatG,
|
||||
$servingCarbG,
|
||||
$servingSugarG,
|
||||
$servingFiberG,
|
||||
$servingProteinG,
|
||||
$servingSaltG,
|
||||
$dailyEnergy,
|
||||
$dailyFat,
|
||||
$dailySatFat,
|
||||
$dailyCarb,
|
||||
$dailySugar,
|
||||
$dailyFiber,
|
||||
$dailyProtein,
|
||||
$dailySalt,
|
||||
];
|
||||
|
||||
foreach ($values as $val) {
|
||||
if ($val !== null && $val < 0) {
|
||||
throw new \DomainException('Nutritional values cannot be negative');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,float|null>
|
||||
* @return array<string, array{value: string, unit: string}|null>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'nv_ref_weight_g' => $this->refWeightG,
|
||||
'nv_kj' => $this->kj,
|
||||
'nv_kcal' => $this->kcal,
|
||||
'nv_fat_g' => $this->fatG,
|
||||
'nv_sat_fat_g' => $this->satFatG,
|
||||
'nv_carb_g' => $this->carbG,
|
||||
'nv_sugar_g' => $this->sugarG,
|
||||
'nv_fiber_g' => $this->fiberG,
|
||||
'nv_protein_g' => $this->proteinG,
|
||||
'nv_salt_g' => $this->saltG,
|
||||
|
||||
'nv_serving_size_g' => $this->servingSizeG,
|
||||
'nv_serving_kj' => $this->servingKj,
|
||||
'nv_serving_kcal' => $this->servingKcal,
|
||||
'nv_serving_fat_g' => $this->servingFatG,
|
||||
'nv_serving_sat_fat_g' => $this->servingSatFatG,
|
||||
'nv_serving_carb_g' => $this->servingCarbG,
|
||||
'nv_serving_sugar_g' => $this->servingSugarG,
|
||||
'nv_serving_fiber_g' => $this->servingFiberG,
|
||||
'nv_serving_protein_g' => $this->servingProteinG,
|
||||
'nv_serving_salt_g' => $this->servingSaltG,
|
||||
|
||||
'nv_daily_energy' => $this->dailyEnergy,
|
||||
'nv_daily_fat' => $this->dailyFat,
|
||||
'nv_daily_sat_fat' => $this->dailySatFat,
|
||||
'nv_daily_carb' => $this->dailyCarb,
|
||||
'nv_daily_sugar' => $this->dailySugar,
|
||||
'nv_daily_fiber' => $this->dailyFiber,
|
||||
'nv_daily_protein' => $this->dailyProtein,
|
||||
'nv_daily_salt' => $this->dailySalt,
|
||||
'refWeightG' => $this->refWeightG->toArray(),
|
||||
'kj' => $this->kj?->toArray(),
|
||||
'kcal' => $this->kcal?->toArray(),
|
||||
'fatG' => $this->fatG?->toArray(),
|
||||
'satFatG' => $this->satFatG?->toArray(),
|
||||
'carbG' => $this->carbG?->toArray(),
|
||||
'sugarG' => $this->sugarG?->toArray(),
|
||||
'fiberG' => $this->fiberG?->toArray(),
|
||||
'proteinG' => $this->proteinG?->toArray(),
|
||||
'saltG' => $this->saltG?->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{value: string, unit: string}|null>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
domain/Models/ValueObjects/Products/NutritionnalUnit.php
Normal file
27
domain/Models/ValueObjects/Products/NutritionnalUnit.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domain\Models\ValueObjects\Products;
|
||||
|
||||
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
|
||||
|
||||
enum NutritionnalUnit: string
|
||||
{
|
||||
use BaseEnum;
|
||||
|
||||
case GRAM = 'g';
|
||||
case KCAL = 'kcal';
|
||||
case KJ = 'kj';
|
||||
|
||||
/**
|
||||
* Optionnal labels definition.
|
||||
*
|
||||
* @phpstan-ignore classConstant.unused
|
||||
*/
|
||||
private const LABELS = [
|
||||
self::GRAM->name => 'g',
|
||||
self::KCAL->name => 'Kcal',
|
||||
self::KJ->name => 'Kj',
|
||||
];
|
||||
}
|
||||
|
|
@ -244,6 +244,17 @@ return [
|
|||
'national_code' => 'code national',
|
||||
'number' => 'numéro',
|
||||
'nutritional_values' => 'valeurs nutritionnelles',
|
||||
'nutritional_refWeightG' => 'poids de référence (g)',
|
||||
'nutritional_kj' => 'kJ',
|
||||
'nutritional_kcal' => 'kcal',
|
||||
'nutritional_fatG' => 'graisses (g)',
|
||||
'nutritional_satFatG' => 'acides gras saturés (g)',
|
||||
'nutritional_carbG' => 'glucides (g)',
|
||||
'nutritional_sugarG' => 'sucres (g)',
|
||||
'nutritional_fiberG' => 'fibres (g)',
|
||||
'nutritional_proteinG' => 'protéines (g)',
|
||||
'nutritional_saltG' => 'sel (g)',
|
||||
'nutritional_values_unit' => 'unité des valeurs nutritionnelles',
|
||||
'password' => 'mot de passe',
|
||||
'password_confirmation' => 'confirmation du mot de passe',
|
||||
'pdf' => 'fiche technique',
|
||||
|
|
|
|||
319
resources/ts/back/Components/Inputs/NutritionalValuesInput.tsx
Normal file
319
resources/ts/back/Components/Inputs/NutritionalValuesInput.tsx
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,7 +20,9 @@ import SubmitButton from "@/back/Components/SubmitButton";
|
|||
import useCrudSubmit from "@/back/hooks/use-crud-submit";
|
||||
// * Types & Global
|
||||
import CompleteInputFile from "@/back/Components/Inputs/CompleteInputFile";
|
||||
import { RawProductModel } from "@/back/types/products";
|
||||
import NutritionalValuesInput from "@/back/Components/Inputs/NutritionalValuesInput";
|
||||
import { NutritionnalValues, RawProductModel } from "@/back/types/products";
|
||||
|
||||
import { ModelId } from "@/domain";
|
||||
import str from "@/hooks/use-string";
|
||||
import trans from "@/hooks/use-translations";
|
||||
|
|
@ -46,6 +48,7 @@ interface FormData {
|
|||
ingredients: string;
|
||||
conservation_before_opening: string;
|
||||
conservation_after_opening: string;
|
||||
nutritional_values: NutritionnalValues | null;
|
||||
pdf: string | File | null;
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +117,7 @@ export default function Form({
|
|||
ingredients: model?.ingredients ?? "",
|
||||
conservation_before_opening: model?.conservation_before_opening ?? "",
|
||||
conservation_after_opening: model?.conservation_after_opening ?? "",
|
||||
nutritional_values: model?.nutritional_values ?? null,
|
||||
pdf: model?.pdf ?? null,
|
||||
}).withPrecognition(precognitionMethod, precognitionUrl);
|
||||
|
||||
|
|
@ -271,6 +275,13 @@ export default function Form({
|
|||
setData={setData}
|
||||
/>
|
||||
|
||||
{/* Nutritionnal Values */}
|
||||
<NutritionalValuesInput
|
||||
value={data.nutritional_values}
|
||||
setData={setData}
|
||||
parentErrors={formErrors}
|
||||
/>
|
||||
|
||||
<SimpleWysiwyg
|
||||
name="details_and_advantages"
|
||||
tableName="products"
|
||||
|
|
|
|||
|
|
@ -12,38 +12,22 @@ import {
|
|||
} from "@/domain";
|
||||
import * as zod from "zod";
|
||||
|
||||
export interface NutritionnalValue {
|
||||
value: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface NutritionnalValues {
|
||||
refWeightG: number;
|
||||
// * Pour 100g ou par référence
|
||||
kj?: number | null;
|
||||
kcal?: number | null;
|
||||
fatG?: number | null;
|
||||
satFatG?: number | null;
|
||||
carbG?: number | null;
|
||||
sugarG?: number | null;
|
||||
fiberG?: number | null;
|
||||
proteinG?: number | null;
|
||||
saltG?: number | null;
|
||||
// * Par portion
|
||||
servingSizeG?: number | null;
|
||||
servingKj?: number | null;
|
||||
servingKcal?: number | null;
|
||||
servingFatG?: number | null;
|
||||
servingSatFatG?: number | null;
|
||||
servingCarbG?: number | null;
|
||||
servingSugarG?: number | null;
|
||||
servingFiberG?: number | null;
|
||||
servingProteinG?: number | null;
|
||||
servingSaltG?: number | null;
|
||||
// * Apports journaliers (% AR)
|
||||
dailyEnergy?: number | null;
|
||||
dailyFat?: number | null;
|
||||
dailySatFat?: number | null;
|
||||
dailyCarb?: number | null;
|
||||
dailySugar?: number | null;
|
||||
dailyFiber?: number | null;
|
||||
dailyProtein?: number | null;
|
||||
dailySalt?: number | null;
|
||||
refWeightG: NutritionnalValue;
|
||||
kj?: NutritionnalValue | null;
|
||||
kcal?: NutritionnalValue | null;
|
||||
fatG?: NutritionnalValue | null;
|
||||
satFatG?: NutritionnalValue | null;
|
||||
carbG?: NutritionnalValue | null;
|
||||
sugarG?: NutritionnalValue | null;
|
||||
fiberG?: NutritionnalValue | null;
|
||||
proteinG?: NutritionnalValue | null;
|
||||
saltG?: NutritionnalValue | null;
|
||||
}
|
||||
|
||||
export interface RawProductModel extends ModelWithPolicies {
|
||||
|
|
@ -77,35 +61,22 @@ export interface ProductModel extends RawProductModel {
|
|||
deleted_at: Date | null;
|
||||
}
|
||||
|
||||
const nutritionalValueSchema = zod.object({
|
||||
value: zod.string(),
|
||||
unit: zod.string(),
|
||||
});
|
||||
|
||||
const nutritionalValuesSchema = zod.object({
|
||||
refWeightG: zod.number(),
|
||||
kj: zod.number().nullable(),
|
||||
kcal: zod.number().nullable(),
|
||||
fatG: zod.number().nullable(),
|
||||
satFatG: zod.number().nullable(),
|
||||
carbG: zod.number().nullable(),
|
||||
sugarG: zod.number().nullable(),
|
||||
fiberG: zod.number().nullable(),
|
||||
proteinG: zod.number().nullable(),
|
||||
saltG: zod.number().nullable(),
|
||||
servingSizeG: zod.number().nullable(),
|
||||
servingKj: zod.number().nullable(),
|
||||
servingKcal: zod.number().nullable(),
|
||||
servingFatG: zod.number().nullable(),
|
||||
servingSatFatG: zod.number().nullable(),
|
||||
servingCarbG: zod.number().nullable(),
|
||||
servingSugarG: zod.number().nullable(),
|
||||
servingFiberG: zod.number().nullable(),
|
||||
servingProteinG: zod.number().nullable(),
|
||||
servingSaltG: zod.number().nullable(),
|
||||
dailyEnergy: zod.number().nullable(),
|
||||
dailyFat: zod.number().nullable(),
|
||||
dailySatFat: zod.number().nullable(),
|
||||
dailyCarb: zod.number().nullable(),
|
||||
dailySugar: zod.number().nullable(),
|
||||
dailyFiber: zod.number().nullable(),
|
||||
dailyProtein: zod.number().nullable(),
|
||||
dailySalt: zod.number().nullable(),
|
||||
refWeightG: nutritionalValueSchema,
|
||||
kj: nutritionalValueSchema.nullable().optional(),
|
||||
kcal: nutritionalValueSchema.nullable().optional(),
|
||||
fatG: nutritionalValueSchema.nullable().optional(),
|
||||
satFatG: nutritionalValueSchema.nullable().optional(),
|
||||
carbG: nutritionalValueSchema.nullable().optional(),
|
||||
sugarG: nutritionalValueSchema.nullable().optional(),
|
||||
fiberG: nutritionalValueSchema.nullable().optional(),
|
||||
proteinG: nutritionalValueSchema.nullable().optional(),
|
||||
saltG: nutritionalValueSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export const rawProductModelSchema = zod.object({
|
||||
|
|
|
|||
|
|
@ -4,35 +4,22 @@ import { BreedingMethod, Business, Technology } from "../value-objects";
|
|||
|
||||
import ProductCategory from "../value-objects/product-category";
|
||||
|
||||
export interface NutritionalValue {
|
||||
value: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface NutritionalValues {
|
||||
refWeightG: number | null;
|
||||
kj: number | null;
|
||||
kcal: number | null;
|
||||
fatG: number | null;
|
||||
satFatG: number | null;
|
||||
carbG: number | null;
|
||||
sugarG: number | null;
|
||||
fiberG: number | null;
|
||||
proteinG: number | null;
|
||||
saltG: number | null;
|
||||
servingSizeG: number | null;
|
||||
servingKj: number | null;
|
||||
servingKcal: number | null;
|
||||
servingFatG: number | null;
|
||||
servingSatFatG: number | null;
|
||||
servingCarbG: number | null;
|
||||
servingSugarG: number | null;
|
||||
servingFiberG: number | null;
|
||||
servingProteinG: number | null;
|
||||
servingSaltG: number | null;
|
||||
dailyEnergy: number | null;
|
||||
dailyFat: number | null;
|
||||
dailySatFat: number | null;
|
||||
dailyCarb: number | null;
|
||||
dailySugar: number | null;
|
||||
dailyFiber: number | null;
|
||||
dailyProtein: number | null;
|
||||
dailySalt: number | null;
|
||||
refWeightG: NutritionalValue;
|
||||
kj: NutritionalValue | null;
|
||||
kcal: NutritionalValue | null;
|
||||
fatG: NutritionalValue | null;
|
||||
satFatG: NutritionalValue | null;
|
||||
carbG: NutritionalValue | null;
|
||||
sugarG: NutritionalValue | null;
|
||||
fiberG: NutritionalValue | null;
|
||||
proteinG: NutritionalValue | null;
|
||||
saltG: NutritionalValue | null;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
|
|
|
|||
|
|
@ -248,73 +248,48 @@ export default function Show({
|
|||
{
|
||||
label: "Énergie",
|
||||
subLabel: null,
|
||||
val1:
|
||||
product.nutritionalValues.kj !== null
|
||||
? `${product.nutritionalValues.kj} Kj`
|
||||
: null,
|
||||
val2:
|
||||
product.nutritionalValues.kcal !== null
|
||||
? `${product.nutritionalValues.kcal} Kcal`
|
||||
: null,
|
||||
val1: product.nutritionalValues.kj,
|
||||
val2: product.nutritionalValues.kcal,
|
||||
show:
|
||||
product.nutritionalValues.kj !== null ||
|
||||
product.nutritionalValues.kcal !== null,
|
||||
Boolean(product.nutritionalValues.kj?.value) ||
|
||||
Boolean(product.nutritionalValues.kcal?.value),
|
||||
},
|
||||
{
|
||||
label: "Matières Grasses",
|
||||
subLabel: "dont Acides Gras Saturés",
|
||||
val1:
|
||||
product.nutritionalValues.fatG !== null
|
||||
? `${product.nutritionalValues.fatG} g`
|
||||
: null,
|
||||
val2:
|
||||
product.nutritionalValues.satFatG !== null
|
||||
? `${product.nutritionalValues.satFatG} g`
|
||||
: null,
|
||||
show: product.nutritionalValues.fatG !== null,
|
||||
val1: product.nutritionalValues.fatG,
|
||||
val2: product.nutritionalValues.satFatG,
|
||||
show: Boolean(product.nutritionalValues.fatG?.value),
|
||||
},
|
||||
{
|
||||
label: "Glucides",
|
||||
subLabel: "dont sucres",
|
||||
val1:
|
||||
product.nutritionalValues.carbG !== null
|
||||
? `${product.nutritionalValues.carbG} g`
|
||||
: null,
|
||||
val2:
|
||||
product.nutritionalValues.sugarG !== null
|
||||
? `${product.nutritionalValues.sugarG} g`
|
||||
: null,
|
||||
show: product.nutritionalValues.carbG !== null,
|
||||
val1: product.nutritionalValues.carbG,
|
||||
val2: product.nutritionalValues.sugarG,
|
||||
show: Boolean(product.nutritionalValues.carbG?.value),
|
||||
},
|
||||
{
|
||||
label: "Fibres alimentaires",
|
||||
subLabel: null,
|
||||
val1:
|
||||
product.nutritionalValues.fiberG !== null
|
||||
? `${product.nutritionalValues.fiberG} g`
|
||||
: null,
|
||||
val1: product.nutritionalValues.fiberG,
|
||||
val2: null,
|
||||
show: product.nutritionalValues.fiberG !== null,
|
||||
show: Boolean(product.nutritionalValues.fiberG?.value),
|
||||
},
|
||||
{
|
||||
label: "Protéines",
|
||||
subLabel: null,
|
||||
val1:
|
||||
product.nutritionalValues.proteinG !== null
|
||||
? `${product.nutritionalValues.proteinG} g`
|
||||
: null,
|
||||
val1: product.nutritionalValues.proteinG,
|
||||
val2: null,
|
||||
show: product.nutritionalValues.proteinG !== null,
|
||||
show: Boolean(
|
||||
product.nutritionalValues.proteinG?.value,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Sel",
|
||||
subLabel: null,
|
||||
val1:
|
||||
product.nutritionalValues.saltG !== null
|
||||
? `${product.nutritionalValues.saltG} g`
|
||||
: null,
|
||||
val1: product.nutritionalValues.saltG,
|
||||
val2: null,
|
||||
show: product.nutritionalValues.saltG !== null,
|
||||
show: Boolean(product.nutritionalValues.saltG?.value),
|
||||
},
|
||||
].filter((r) => r.show)
|
||||
: [];
|
||||
|
|
@ -347,8 +322,12 @@ export default function Show({
|
|||
{rows.length > 0 && (
|
||||
<div className="w-full">
|
||||
<div className="text-right font-bold text-primary-foreground mb-2 text-xs md:text-sm">
|
||||
Pour 100g
|
||||
Pour{" "}
|
||||
{product.nutritionalValues?.refWeightG?.value
|
||||
? `${product.nutritionalValues.refWeightG.value} ${product.nutritionalValues.refWeightG.unit}`
|
||||
: "100g"}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{rows.map((row, idx) => (
|
||||
<div
|
||||
|
|
@ -356,7 +335,7 @@ export default function Show({
|
|||
className={`flex justify-between items-center p-2.5 md:p-4 font-bold ${idx % 2 === 0 ? "bg-primary-foreground text-primary" : "bg-white text-primary-foreground"}`}
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm md:text-base">
|
||||
<div className="text-sm md:text-base capitalize">
|
||||
{row.label}
|
||||
</div>
|
||||
{row.subLabel && row.val2 !== null && (
|
||||
|
|
@ -366,10 +345,36 @@ export default function Show({
|
|||
)}
|
||||
</div>
|
||||
<div className="text-right text-sm md:text-base">
|
||||
{row.val1 !== null && <div>{row.val1}</div>}
|
||||
{row.val1 !== null && (
|
||||
<div>
|
||||
<span className="pe-1">
|
||||
{row.val1.value}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
row.val1.unit.length > 1
|
||||
? "capitalize"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{row.val1.unit}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{row.val2 !== null && (
|
||||
<div className="font-normal text-xs">
|
||||
{row.val2}
|
||||
<div>
|
||||
<span className="pe-1">
|
||||
{row.val2.value}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
row.val2.unit.length > 1
|
||||
? "capitalize"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{row.val2.unit}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,35 +10,22 @@ import {
|
|||
} from "@/domain";
|
||||
import ProductCategory from "../../domain/value-objects/product-category";
|
||||
|
||||
export const NutritionalValueSchema = zod.object({
|
||||
value: zod.string(),
|
||||
unit: zod.string(),
|
||||
});
|
||||
|
||||
export const NutritionalValuesSchema = zod.object({
|
||||
refWeightG: zod.number().nullable(),
|
||||
kj: zod.number().nullable(),
|
||||
kcal: zod.number().nullable(),
|
||||
fatG: zod.number().nullable(),
|
||||
satFatG: zod.number().nullable(),
|
||||
carbG: zod.number().nullable(),
|
||||
sugarG: zod.number().nullable(),
|
||||
fiberG: zod.number().nullable(),
|
||||
proteinG: zod.number().nullable(),
|
||||
saltG: zod.number().nullable(),
|
||||
servingSizeG: zod.number().nullable(),
|
||||
servingKj: zod.number().nullable(),
|
||||
servingKcal: zod.number().nullable(),
|
||||
servingFatG: zod.number().nullable(),
|
||||
servingSatFatG: zod.number().nullable(),
|
||||
servingCarbG: zod.number().nullable(),
|
||||
servingSugarG: zod.number().nullable(),
|
||||
servingFiberG: zod.number().nullable(),
|
||||
servingProteinG: zod.number().nullable(),
|
||||
servingSaltG: zod.number().nullable(),
|
||||
dailyEnergy: zod.number().nullable(),
|
||||
dailyFat: zod.number().nullable(),
|
||||
dailySatFat: zod.number().nullable(),
|
||||
dailyCarb: zod.number().nullable(),
|
||||
dailySugar: zod.number().nullable(),
|
||||
dailyFiber: zod.number().nullable(),
|
||||
dailyProtein: zod.number().nullable(),
|
||||
dailySalt: zod.number().nullable(),
|
||||
refWeightG: NutritionalValueSchema,
|
||||
kj: NutritionalValueSchema.nullable(),
|
||||
kcal: NutritionalValueSchema.nullable(),
|
||||
fatG: NutritionalValueSchema.nullable(),
|
||||
satFatG: NutritionalValueSchema.nullable(),
|
||||
carbG: NutritionalValueSchema.nullable(),
|
||||
sugarG: NutritionalValueSchema.nullable(),
|
||||
fiberG: NutritionalValueSchema.nullable(),
|
||||
proteinG: NutritionalValueSchema.nullable(),
|
||||
saltG: NutritionalValueSchema.nullable(),
|
||||
});
|
||||
|
||||
export const ProductSchema = zod.object({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue