From 1a9895746610a90467dc4f92abaad2e07a9ec515 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Thu, 12 Feb 2026 17:39:53 +0100 Subject: [PATCH] add products --- backend/src/models.py | 22 ++++---- backend/src/productors/service.py | 4 +- backend/src/products/products.py | 10 ++-- backend/src/products/service.py | 10 +++- .../components/Productors/Filter/index.tsx | 4 +- .../src/components/Products/Filter/index.tsx | 20 +++---- .../src/components/Products/Modal/index.tsx | 45 +++++++++------- .../src/components/Products/Row/index.tsx | 8 +-- frontend/src/pages/Dashboard/index.tsx | 8 +-- frontend/src/pages/Productors/index.tsx | 1 - frontend/src/pages/Products/index.tsx | 18 +++---- frontend/src/services/resources/products.ts | 54 ++++++++++++++++--- 12 files changed, 129 insertions(+), 75 deletions(-) diff --git a/backend/src/models.py b/backend/src/models.py index cef2eb4..8d8e7ce 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -1,5 +1,5 @@ from sqlmodel import Field, SQLModel, Relationship -from enum import Enum +from enum import StrEnum from typing import Optional import datetime @@ -44,14 +44,14 @@ class ProductorUpdate(SQLModel): class ProductorCreate(ProductorBase): pass -class Unit(Enum): - GRAMS = 1 - KILO = 2 - PIECE = 3 +class Unit(StrEnum): + GRAMS = "1" + KILO = "2" + PIECE = "3" -class ProductType(Enum): - PLANNED = 1 - RECCURENT = 2 +class ProductType(StrEnum): + PLANNED = "1" + RECCURENT = "2" class ShipmentProductLink(SQLModel, table=True): shipment_id: Optional[int] = Field(default=None, foreign_key="shipment.id", primary_key=True) @@ -62,7 +62,7 @@ class ProductBase(SQLModel): unit: Unit price: float price_kg: float | None - weight: float + weight: float | None type: ProductType productor_id: int | None = Field(default=None, foreign_key="productor.id") @@ -83,10 +83,10 @@ class ProductUpdate(SQLModel): price_kg: float | None weight: float | None productor_id: int | None - shipment_ids: list[int] | None + shipment_ids: list[int] | None = [] class ProductCreate(ProductBase): - shipment_ids: list[int] | None + shipment_ids: list[int] | None = [] class FormBase(SQLModel): name: str diff --git a/backend/src/productors/service.py b/backend/src/productors/service.py index 85604a8..0d3aa55 100644 --- a/backend/src/productors/service.py +++ b/backend/src/productors/service.py @@ -4,9 +4,9 @@ import src.models as models def get_all(session: Session, names: list[str], types: list[str]) -> list[models.ProductorPublic]: statement = select(models.Productor) if len(names) > 0: - statement.where(models.Productor.name.in_(names)) + statement = statement.where(models.Productor.name.in_(names)) if len(types) > 0: - statement.where(models.Productor.type.in_(types)) + statement = statement.where(models.Productor.type.in_(types)) return session.exec(statement).all() def get_one(session: Session, productor_id: int) -> models.ProductorPublic: diff --git a/backend/src/products/products.py b/backend/src/products/products.py index dc61267..902af0f 100644 --- a/backend/src/products/products.py +++ b/backend/src/products/products.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, Query import src.messages as messages import src.models as models from src.database import get_session @@ -8,8 +8,12 @@ from src.auth.auth import get_current_user router = APIRouter(prefix='/products') #user=Depends(get_current_user) @router.get('/', response_model=list[models.ProductPublic], ) -def get_products(session: Session = Depends(get_session)): - return service.get_all(session) +def get_products( + session: Session = Depends(get_session), + names: list[str] = Query([]), + productors: list[str] = Query([]), +): + return service.get_all(session, names, productors) @router.get('/{id}', response_model=models.ProductPublic) def get_product(id: int, session: Session = Depends(get_session)): diff --git a/backend/src/products/service.py b/backend/src/products/service.py index 8eaa06b..4470c10 100644 --- a/backend/src/products/service.py +++ b/backend/src/products/service.py @@ -1,8 +1,16 @@ from sqlmodel import Session, select import src.models as models -def get_all(session: Session) -> list[models.ProductPublic]: +def get_all( + session: Session, + names: list[str], + productors: list[str] +) -> list[models.ProductPublic]: statement = select(models.Product) + if len(names) > 0: + statement = statement.where(models.Product.name.in_(names)) + if len(productors) > 0: + statement = statement.join(models.Productor).where(models.Productor.name.in_(productors)) return session.exec(statement).all() def get_one(session: Session, product_id: int) -> models.ProductPublic: diff --git a/frontend/src/components/Productors/Filter/index.tsx b/frontend/src/components/Productors/Filter/index.tsx index d120fa5..8bc9e04 100644 --- a/frontend/src/components/Productors/Filter/index.tsx +++ b/frontend/src/components/Productors/Filter/index.tsx @@ -16,10 +16,10 @@ export default function ProductorsFilter({ onFilterChange }: ProductorsFiltersProps) { const defaultNames = useMemo(() => { - return filters.getAll("name") + return filters.getAll("names") }, [filters]); const defaultTypes = useMemo(() => { - return filters.getAll("type") + return filters.getAll("types") }, [filters]); return ( diff --git a/frontend/src/components/Products/Filter/index.tsx b/frontend/src/components/Products/Filter/index.tsx index ccbda7f..b00a672 100644 --- a/frontend/src/components/Products/Filter/index.tsx +++ b/frontend/src/components/Products/Filter/index.tsx @@ -4,22 +4,22 @@ import { t } from "@/config/i18n"; export type ProductsFiltersProps = { names: string[]; - types: string[]; + productors: string[]; filters: URLSearchParams; onFilterChange: (values: string[], filter: string) => void; } export default function ProductsFilters({ names, - types, + productors, filters, onFilterChange }: ProductsFiltersProps) { const defaultNames = useMemo(() => { - return filters.getAll("name") + return filters.getAll("names") }, [filters]); - const defaultTypes = useMemo(() => { - return filters.getAll("type") + const defaultProductors = useMemo(() => { + return filters.getAll("productors") }, [filters]); return ( @@ -34,12 +34,12 @@ export default function ProductsFilters({ clearable /> { - onFilterChange(values, 'types') + onFilterChange(values, 'productors') }} clearable /> diff --git a/frontend/src/components/Products/Modal/index.tsx b/frontend/src/components/Products/Modal/index.tsx index ec8b133..8267a71 100644 --- a/frontend/src/components/Products/Modal/index.tsx +++ b/frontend/src/components/Products/Modal/index.tsx @@ -2,7 +2,7 @@ import { Button, Group, Modal, NumberInput, Select, TextInput, Title, type Modal import { t } from "@/config/i18n"; import { useForm } from "@mantine/form"; import { IconCancel } from "@tabler/icons-react"; -import type { Product, ProductInputs } from "@/services/resources/products"; +import { productToProductInputs, type Product, type ProductInputs } from "@/services/resources/products"; import { useEffect, useMemo } from "react"; import { getProductors } from "@/services/api"; @@ -23,9 +23,9 @@ export function ProductModal({ name: "", unit: null, price: null, - priceKg: null, + price_kg: null, weight: null, - type: "", + type: null, productor_id: null, }, validate: { @@ -35,10 +35,6 @@ export function ProductModal({ !value ? `${t("unit", {capfirst: true})} ${t('is required')}` : null, price: (value) => !value ? `${t("price", {capfirst: true})} ${t('is required')}` : null, - priceKg: (value) => - !value ? `${t("priceKg", {capfirst: true})} ${t('is required')}` : null, - weight: (value) => - !value ? `${t("weight", {capfirst: true})} ${t('is required')}` : null, type: (value) => !value ? `${t("type", {capfirst: true})} ${t('is required')}` : null, productor_id: (value) => @@ -48,9 +44,7 @@ export function ProductModal({ useEffect(() => { if (currentProduct) { - form.initialize({ - ...currentProduct, - }); + form.initialize(productToProductInputs(currentProduct)); } }, [currentProduct]); @@ -73,13 +67,27 @@ export function ProductModal({ withAsterisk {...form.getInputProps('name')} /> - + { form.validate(); + console.log(form.isValid(), form.getValues()) if (form.isValid()) handleSubmit(form.getValues(), currentProduct?.id) }} diff --git a/frontend/src/components/Products/Row/index.tsx b/frontend/src/components/Products/Row/index.tsx index b3d97d2..242bc43 100644 --- a/frontend/src/components/Products/Row/index.tsx +++ b/frontend/src/components/Products/Row/index.tsx @@ -1,7 +1,7 @@ import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { t } from "@/config/i18n"; import { IconEdit, IconX } from "@tabler/icons-react"; -import type { Product, ProductInputs } from "@/services/resources/products"; +import { ProductType, ProductUnit, type Product, type ProductInputs } from "@/services/resources/products"; import { ProductModal } from "@/components/Products/Modal"; import { deleteProduct, getProduct } from "@/services/api"; import { useNavigate } from "react-router"; @@ -32,11 +32,11 @@ export default function ProductRow({ handleSubmit={handleSubmit} /> {product.name} - {product.type} + {ProductType[product.type]} {product.price} - {product.priceKg} + {product.price_kg} {product.weight} - {product.unit} + {ProductUnit[product.unit]} navigate(`/dashboard/${value}`)} > diff --git a/frontend/src/pages/Productors/index.tsx b/frontend/src/pages/Productors/index.tsx index 020bf3f..2baedc6 100644 --- a/frontend/src/pages/Productors/index.tsx +++ b/frontend/src/pages/Productors/index.tsx @@ -62,7 +62,6 @@ export default function Productors() { values.forEach(value => { params.append(filter, value); }); - return params; }); }, [searchParams, setSearchParams]) diff --git a/frontend/src/pages/Products/index.tsx b/frontend/src/pages/Products/index.tsx index c6aa517..6a56470 100644 --- a/frontend/src/pages/Products/index.tsx +++ b/frontend/src/pages/Products/index.tsx @@ -6,7 +6,7 @@ import ProductRow from "@/components/Products/Row"; import { useLocation, useNavigate, useSearchParams } from "react-router"; import { ProductModal } from "@/components/Products/Modal"; import { useCallback, useMemo } from "react"; -import type { Product, ProductInputs } from "@/services/resources/products"; +import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products"; import ProductsFilters from "@/components/Products/Filter"; export default function Products() { @@ -28,19 +28,17 @@ export default function Products() { return allProducts?.map((product: Product) => (product.name)) .filter((season, index, array) => array.indexOf(season) === index) }, [allProducts]) - - const types = useMemo(() => { - return allProducts?.map((product: Product) => (product.type)) - .filter((product, index, array) => array.indexOf(product) === index) + + const productors = useMemo(() => { + return allProducts?.map((form: Product) => (form.productor.name)) + .filter((productor, index, array) => array.indexOf(productor) === index) }, [allProducts]) const createProductMutation = createProduct(); const editProductMutation = editProduct(); const handleCreateProduct = useCallback(async (product: ProductInputs) => { - await createProductMutation.mutateAsync({ - ...product - }); + await createProductMutation.mutateAsync(productCreateFromProductInputs(product)); closeModal(); }, [createProductMutation]); @@ -57,7 +55,7 @@ export default function Products() { const onFilterChange = useCallback((values: string[], filter: string) => { setSearchParams(prev => { const params = new URLSearchParams(prev); - params.delete(filter) + params.delete(filter); values.forEach(value => { params.append(filter, value); @@ -91,8 +89,8 @@ export default function Products() { /> diff --git a/frontend/src/services/resources/products.ts b/frontend/src/services/resources/products.ts index 2b11198..aed7a31 100644 --- a/frontend/src/services/resources/products.ts +++ b/frontend/src/services/resources/products.ts @@ -1,34 +1,48 @@ +import { t } from "@/config/i18n"; import type { Productor } from "@/services/resources/productors"; import type { Shipment } from "@/services/resources/shipments"; +export const ProductType = [ + "none", + t("planned"), + t("reccurent"), +]; + +export const ProductUnit = [ + "none", + t("grams"), + t("kilo"), + t("piece"), +]; + export type Product = { id: number; productor: Productor; name: string; unit: number; price: number; - priceKg: number | null; + price_kg: number | null; weight: number; type: number; shipments: Shipment[]; } export type ProductCreate = { - productor_id: Productor; + productor_id: number; name: string; unit: number; price: number; - priceKg: number | null; - weight: number; + price_kg: number | null; + weight: number | null; type: number; } export type ProductEdit = { - productor_id: Productor | null; + productor_id: number | null; name: string | null; unit: number | null; price: number | null; - priceKg: number | null; + price_kg: number | null; weight: number | null; type: number | null; } @@ -38,12 +52,36 @@ export type ProductInputs = { name: string; unit: number | null; price: number | null; - priceKg: number | null; + price_kg: number | null; weight: number | null; - type: string; + type: number | null; } export type ProductEditPayload = { product: ProductEdit; id: number; +} + +export function productToProductInputs(product: Product): ProductInputs { + return { + productor_id: product.productor.id, + name: product.name, + unit: product.unit, + price: product.price, + price_kg: product.price_kg, + weight: product.weight, + type: product.type, + }; +} + +export function productCreateFromProductInputs(productInput: ProductInputs): ProductCreate { + return { + productor_id: productInput.productor_id!, + name: productInput.name, + unit: productInput.unit!, + price: productInput.price!, + price_kg: productInput.price_kg, + weight: productInput.weight, + type: productInput.type!, + } } \ No newline at end of file