add products

This commit is contained in:
Julien Aldon
2026-02-12 17:39:53 +01:00
parent 025b78d5dd
commit 1a98957466
12 changed files with 129 additions and 75 deletions

View File

@@ -1,5 +1,5 @@
from sqlmodel import Field, SQLModel, Relationship from sqlmodel import Field, SQLModel, Relationship
from enum import Enum from enum import StrEnum
from typing import Optional from typing import Optional
import datetime import datetime
@@ -44,14 +44,14 @@ class ProductorUpdate(SQLModel):
class ProductorCreate(ProductorBase): class ProductorCreate(ProductorBase):
pass pass
class Unit(Enum): class Unit(StrEnum):
GRAMS = 1 GRAMS = "1"
KILO = 2 KILO = "2"
PIECE = 3 PIECE = "3"
class ProductType(Enum): class ProductType(StrEnum):
PLANNED = 1 PLANNED = "1"
RECCURENT = 2 RECCURENT = "2"
class ShipmentProductLink(SQLModel, table=True): class ShipmentProductLink(SQLModel, table=True):
shipment_id: Optional[int] = Field(default=None, foreign_key="shipment.id", primary_key=True) shipment_id: Optional[int] = Field(default=None, foreign_key="shipment.id", primary_key=True)
@@ -62,7 +62,7 @@ class ProductBase(SQLModel):
unit: Unit unit: Unit
price: float price: float
price_kg: float | None price_kg: float | None
weight: float weight: float | None
type: ProductType type: ProductType
productor_id: int | None = Field(default=None, foreign_key="productor.id") productor_id: int | None = Field(default=None, foreign_key="productor.id")
@@ -83,10 +83,10 @@ class ProductUpdate(SQLModel):
price_kg: float | None price_kg: float | None
weight: float | None weight: float | None
productor_id: int | None productor_id: int | None
shipment_ids: list[int] | None shipment_ids: list[int] | None = []
class ProductCreate(ProductBase): class ProductCreate(ProductBase):
shipment_ids: list[int] | None shipment_ids: list[int] | None = []
class FormBase(SQLModel): class FormBase(SQLModel):
name: str name: str

View File

@@ -4,9 +4,9 @@ import src.models as models
def get_all(session: Session, names: list[str], types: list[str]) -> list[models.ProductorPublic]: def get_all(session: Session, names: list[str], types: list[str]) -> list[models.ProductorPublic]:
statement = select(models.Productor) statement = select(models.Productor)
if len(names) > 0: if len(names) > 0:
statement.where(models.Productor.name.in_(names)) statement = statement.where(models.Productor.name.in_(names))
if len(types) > 0: if len(types) > 0:
statement.where(models.Productor.type.in_(types)) statement = statement.where(models.Productor.type.in_(types))
return session.exec(statement).all() return session.exec(statement).all()
def get_one(session: Session, productor_id: int) -> models.ProductorPublic: def get_one(session: Session, productor_id: int) -> models.ProductorPublic:

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages import src.messages as messages
import src.models as models import src.models as models
from src.database import get_session from src.database import get_session
@@ -8,8 +8,12 @@ from src.auth.auth import get_current_user
router = APIRouter(prefix='/products') router = APIRouter(prefix='/products')
#user=Depends(get_current_user) #user=Depends(get_current_user)
@router.get('/', response_model=list[models.ProductPublic], ) @router.get('/', response_model=list[models.ProductPublic], )
def get_products(session: Session = Depends(get_session)): def get_products(
return service.get_all(session) 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) @router.get('/{id}', response_model=models.ProductPublic)
def get_product(id: int, session: Session = Depends(get_session)): def get_product(id: int, session: Session = Depends(get_session)):

View File

@@ -1,8 +1,16 @@
from sqlmodel import Session, select from sqlmodel import Session, select
import src.models as models 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) 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() return session.exec(statement).all()
def get_one(session: Session, product_id: int) -> models.ProductPublic: def get_one(session: Session, product_id: int) -> models.ProductPublic:

View File

@@ -16,10 +16,10 @@ export default function ProductorsFilter({
onFilterChange onFilterChange
}: ProductorsFiltersProps) { }: ProductorsFiltersProps) {
const defaultNames = useMemo(() => { const defaultNames = useMemo(() => {
return filters.getAll("name") return filters.getAll("names")
}, [filters]); }, [filters]);
const defaultTypes = useMemo(() => { const defaultTypes = useMemo(() => {
return filters.getAll("type") return filters.getAll("types")
}, [filters]); }, [filters]);
return ( return (
<Group> <Group>

View File

@@ -4,22 +4,22 @@ import { t } from "@/config/i18n";
export type ProductsFiltersProps = { export type ProductsFiltersProps = {
names: string[]; names: string[];
types: string[]; productors: string[];
filters: URLSearchParams; filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} }
export default function ProductsFilters({ export default function ProductsFilters({
names, names,
types, productors,
filters, filters,
onFilterChange onFilterChange
}: ProductsFiltersProps) { }: ProductsFiltersProps) {
const defaultNames = useMemo(() => { const defaultNames = useMemo(() => {
return filters.getAll("name") return filters.getAll("names")
}, [filters]); }, [filters]);
const defaultTypes = useMemo(() => { const defaultProductors = useMemo(() => {
return filters.getAll("type") return filters.getAll("productors")
}, [filters]); }, [filters]);
return ( return (
<Group> <Group>
@@ -34,12 +34,12 @@ export default function ProductsFilters({
clearable clearable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by type", {capfirst: true})} aria-label={t("filter by productor", {capfirst: true})}
placeholder={t("filter by type", {capfirst: true})} placeholder={t("filter by productor", {capfirst: true})}
data={types} data={productors}
defaultValue={defaultTypes} defaultValue={defaultProductors}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'types') onFilterChange(values, 'productors')
}} }}
clearable clearable
/> />

View File

@@ -2,7 +2,7 @@ import { Button, Group, Modal, NumberInput, Select, TextInput, Title, type Modal
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react"; 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 { useEffect, useMemo } from "react";
import { getProductors } from "@/services/api"; import { getProductors } from "@/services/api";
@@ -23,9 +23,9 @@ export function ProductModal({
name: "", name: "",
unit: null, unit: null,
price: null, price: null,
priceKg: null, price_kg: null,
weight: null, weight: null,
type: "", type: null,
productor_id: null, productor_id: null,
}, },
validate: { validate: {
@@ -35,10 +35,6 @@ export function ProductModal({
!value ? `${t("unit", {capfirst: true})} ${t('is required')}` : null, !value ? `${t("unit", {capfirst: true})} ${t('is required')}` : null,
price: (value) => price: (value) =>
!value ? `${t("price", {capfirst: true})} ${t('is required')}` : null, !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) => type: (value) =>
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null, !value ? `${t("type", {capfirst: true})} ${t('is required')}` : null,
productor_id: (value) => productor_id: (value) =>
@@ -48,9 +44,7 @@ export function ProductModal({
useEffect(() => { useEffect(() => {
if (currentProduct) { if (currentProduct) {
form.initialize({ form.initialize(productToProductInputs(currentProduct));
...currentProduct,
});
} }
}, [currentProduct]); }, [currentProduct]);
@@ -73,13 +67,27 @@ export function ProductModal({
withAsterisk withAsterisk
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <Select
label={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})} label={t("product type", {capfirst: true})}
placeholder={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})} placeholder={t("product type", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk data={[
{value: "1", label: t("planned")},
{value: "2", label: t("reccurent")}
]}
{...form.getInputProps('type')} {...form.getInputProps('type')}
/> />
<Select
label={t("product unit", {capfirst: true})}
placeholder={t("product unit", {capfirst: true})}
radius="sm"
data={[
{value: "1", label: t("grams")},
{value: "2", label: t("kilo")},
{value: "3", label: t("piece")}
]}
{...form.getInputProps('unit')}
/>
<NumberInput <NumberInput
label={t("product price", {capfirst: true})} label={t("product price", {capfirst: true})}
placeholder={t("product price", {capfirst: true})} placeholder={t("product price", {capfirst: true})}
@@ -88,17 +96,15 @@ export function ProductModal({
{...form.getInputProps('price')} {...form.getInputProps('price')}
/> />
<NumberInput <NumberInput
label={t("product priceKg", {capfirst: true}, {capfirst: true})} label={t("product priceKg", {capfirst: true})}
placeholder={t("product priceKg", {capfirst: true}, {capfirst: true})} placeholder={t("product priceKg", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk {...form.getInputProps('price_kg')}
{...form.getInputProps('priceKg')}
/> />
<NumberInput <NumberInput
label={t("product weight", {capfirst: true})} label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})} placeholder={t("product weight", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk
{...form.getInputProps('weight', {capfirst: true})} {...form.getInputProps('weight', {capfirst: true})}
/> />
<Select <Select
@@ -129,6 +135,7 @@ export function ProductModal({
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})} aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
onClick={() => { onClick={() => {
form.validate(); form.validate();
console.log(form.isValid(), form.getValues())
if (form.isValid()) if (form.isValid())
handleSubmit(form.getValues(), currentProduct?.id) handleSubmit(form.getValues(), currentProduct?.id)
}} }}

View File

@@ -1,7 +1,7 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react"; 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 { ProductModal } from "@/components/Products/Modal";
import { deleteProduct, getProduct } from "@/services/api"; import { deleteProduct, getProduct } from "@/services/api";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
@@ -32,11 +32,11 @@ export default function ProductRow({
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
/> />
<Table.Td>{product.name}</Table.Td> <Table.Td>{product.name}</Table.Td>
<Table.Td>{product.type}</Table.Td> <Table.Td>{ProductType[product.type]}</Table.Td>
<Table.Td>{product.price}</Table.Td> <Table.Td>{product.price}</Table.Td>
<Table.Td>{product.priceKg}</Table.Td> <Table.Td>{product.price_kg}</Table.Td>
<Table.Td>{product.weight}</Table.Td> <Table.Td>{product.weight}</Table.Td>
<Table.Td>{product.unit}</Table.Td> <Table.Td>{ProductUnit[product.unit]}</Table.Td>
<Table.Td> <Table.Td>
<Tooltip label={t("edit product", {capfirst: true})}> <Tooltip label={t("edit product", {capfirst: true})}>
<ActionIcon <ActionIcon

View File

@@ -1,18 +1,18 @@
import { Tabs } from "@mantine/core"; import { Tabs } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { Outlet, useNavigate, useParams } from "react-router"; import { Outlet, useLocation, useNavigate } from "react-router";
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const { tabValue } = useParams(); const location = useLocation();
return ( return (
<Tabs <Tabs
w={{base: "100%", md: "80%", lg: "60%"}} w={{base: "100%", md: "80%", lg: "60%"}}
keepMounted={false} keepMounted={false}
defaultValue="productors"
orientation={"horizontal"} orientation={"horizontal"}
value={tabValue} value={location.pathname.split('/')[2]}
defaultValue={location.pathname.split('/')[2]}
onChange={(value) => navigate(`/dashboard/${value}`)} onChange={(value) => navigate(`/dashboard/${value}`)}
> >
<Tabs.List> <Tabs.List>

View File

@@ -62,7 +62,6 @@ export default function Productors() {
values.forEach(value => { values.forEach(value => {
params.append(filter, value); params.append(filter, value);
}); });
return params; return params;
}); });
}, [searchParams, setSearchParams]) }, [searchParams, setSearchParams])

View File

@@ -6,7 +6,7 @@ import ProductRow from "@/components/Products/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { ProductModal } from "@/components/Products/Modal"; import { ProductModal } from "@/components/Products/Modal";
import { useCallback, useMemo } from "react"; 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"; import ProductsFilters from "@/components/Products/Filter";
export default function Products() { export default function Products() {
@@ -29,18 +29,16 @@ export default function Products() {
.filter((season, index, array) => array.indexOf(season) === index) .filter((season, index, array) => array.indexOf(season) === index)
}, [allProducts]) }, [allProducts])
const types = useMemo(() => { const productors = useMemo(() => {
return allProducts?.map((product: Product) => (product.type)) return allProducts?.map((form: Product) => (form.productor.name))
.filter((product, index, array) => array.indexOf(product) === index) .filter((productor, index, array) => array.indexOf(productor) === index)
}, [allProducts]) }, [allProducts])
const createProductMutation = createProduct(); const createProductMutation = createProduct();
const editProductMutation = editProduct(); const editProductMutation = editProduct();
const handleCreateProduct = useCallback(async (product: ProductInputs) => { const handleCreateProduct = useCallback(async (product: ProductInputs) => {
await createProductMutation.mutateAsync({ await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
...product
});
closeModal(); closeModal();
}, [createProductMutation]); }, [createProductMutation]);
@@ -57,7 +55,7 @@ export default function Products() {
const onFilterChange = useCallback((values: string[], filter: string) => { const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => { setSearchParams(prev => {
const params = new URLSearchParams(prev); const params = new URLSearchParams(prev);
params.delete(filter) params.delete(filter);
values.forEach(value => { values.forEach(value => {
params.append(filter, value); params.append(filter, value);
@@ -91,8 +89,8 @@ export default function Products() {
/> />
</Group> </Group>
<ProductsFilters <ProductsFilters
productors = {productors || []}
names={names || []} names={names || []}
types={types || []}
filters={searchParams} filters={searchParams}
onFilterChange={onFilterChange} onFilterChange={onFilterChange}
/> />

View File

@@ -1,34 +1,48 @@
import { t } from "@/config/i18n";
import type { Productor } from "@/services/resources/productors"; import type { Productor } from "@/services/resources/productors";
import type { Shipment } from "@/services/resources/shipments"; 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 = { export type Product = {
id: number; id: number;
productor: Productor; productor: Productor;
name: string; name: string;
unit: number; unit: number;
price: number; price: number;
priceKg: number | null; price_kg: number | null;
weight: number; weight: number;
type: number; type: number;
shipments: Shipment[]; shipments: Shipment[];
} }
export type ProductCreate = { export type ProductCreate = {
productor_id: Productor; productor_id: number;
name: string; name: string;
unit: number; unit: number;
price: number; price: number;
priceKg: number | null; price_kg: number | null;
weight: number; weight: number | null;
type: number; type: number;
} }
export type ProductEdit = { export type ProductEdit = {
productor_id: Productor | null; productor_id: number | null;
name: string | null; name: string | null;
unit: number | null; unit: number | null;
price: number | null; price: number | null;
priceKg: number | null; price_kg: number | null;
weight: number | null; weight: number | null;
type: number | null; type: number | null;
} }
@@ -38,12 +52,36 @@ export type ProductInputs = {
name: string; name: string;
unit: number | null; unit: number | null;
price: number | null; price: number | null;
priceKg: number | null; price_kg: number | null;
weight: number | null; weight: number | null;
type: string; type: number | null;
} }
export type ProductEditPayload = { export type ProductEditPayload = {
product: ProductEdit; product: ProductEdit;
id: number; 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!,
}
}