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 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

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]:
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:

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.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)):

View File

@@ -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:

View File

@@ -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 (
<Group>

View File

@@ -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 (
<Group>
@@ -34,12 +34,12 @@ export default function ProductsFilters({
clearable
/>
<MultiSelect
aria-label={t("filter by type", {capfirst: true})}
placeholder={t("filter by type", {capfirst: true})}
data={types}
defaultValue={defaultTypes}
aria-label={t("filter by productor", {capfirst: true})}
placeholder={t("filter by productor", {capfirst: true})}
data={productors}
defaultValue={defaultProductors}
onChange={(values: string[]) => {
onFilterChange(values, 'types')
onFilterChange(values, 'productors')
}}
clearable
/>

View File

@@ -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')}
/>
<TextInput
label={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})}
placeholder={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})}
<Select
label={t("product type", {capfirst: true})}
placeholder={t("product type", {capfirst: true})}
radius="sm"
withAsterisk
data={[
{value: "1", label: t("planned")},
{value: "2", label: t("reccurent")}
]}
{...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
label={t("product price", {capfirst: true})}
placeholder={t("product price", {capfirst: true})}
@@ -88,17 +96,15 @@ export function ProductModal({
{...form.getInputProps('price')}
/>
<NumberInput
label={t("product priceKg", {capfirst: true}, {capfirst: true})}
placeholder={t("product priceKg", {capfirst: true}, {capfirst: true})}
label={t("product priceKg", {capfirst: true})}
placeholder={t("product priceKg", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('priceKg')}
{...form.getInputProps('price_kg')}
/>
<NumberInput
label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('weight', {capfirst: true})}
/>
<Select
@@ -129,6 +135,7 @@ export function ProductModal({
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
onClick={() => {
form.validate();
console.log(form.isValid(), form.getValues())
if (form.isValid())
handleSubmit(form.getValues(), currentProduct?.id)
}}

View File

@@ -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}
/>
<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.priceKg}</Table.Td>
<Table.Td>{product.price_kg}</Table.Td>
<Table.Td>{product.weight}</Table.Td>
<Table.Td>{product.unit}</Table.Td>
<Table.Td>{ProductUnit[product.unit]}</Table.Td>
<Table.Td>
<Tooltip label={t("edit product", {capfirst: true})}>
<ActionIcon

View File

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

View File

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

View File

@@ -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() {
@@ -29,18 +29,16 @@ export default function Products() {
.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() {
/>
</Group>
<ProductsFilters
productors = {productors || []}
names={names || []}
types={types || []}
filters={searchParams}
onFilterChange={onFilterChange}
/>

View File

@@ -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!,
}
}