add contract page with dynamic form elements

This commit is contained in:
Julien Aldon
2026-02-13 17:46:24 +01:00
parent ef7403f213
commit 7e42fbe106
34 changed files with 540 additions and 263 deletions

View File

@@ -58,7 +58,6 @@ def callback(code: str, session: Session = Depends(get_session)):
email=decoded_token.get("email"), email=decoded_token.get("email"),
name=decoded_token.get("preferred_username") name=decoded_token.get("preferred_username")
) )
print(user_create)
user = service.get_or_create_user(session, user_create) user = service.get_or_create_user(session, user_create)
return { return {
"access_token": token_data["access_token"], "access_token": token_data["access_token"],

View File

@@ -11,7 +11,7 @@ def get_all(
statement = statement.where(models.Form.season.in_(seasons)) statement = statement.where(models.Form.season.in_(seasons))
if len(productors) > 0: if len(productors) > 0:
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors)) statement = statement.join(models.Productor).where(models.Productor.name.in_(productors))
return session.exec(statement).all() return session.exec(statement.order_by(models.Form.name)).all()
def get_one(session: Session, form_id: int) -> models.FormPublic: def get_one(session: Session, form_id: int) -> models.FormPublic:
return session.get(models.Form, form_id) return session.get(models.Form, form_id)

View File

@@ -33,7 +33,12 @@ class ProductorPublic(ProductorBase):
class Productor(ProductorBase, table=True): class Productor(ProductorBase, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
products: list["Product"] = Relationship(back_populates='productor') products: list["Product"] = Relationship(
back_populates='productor',
sa_relationship_kwargs={
"order_by": "Product.name"
},
)
class ProductorUpdate(SQLModel): class ProductorUpdate(SQLModel):
name: str | None name: str | None
@@ -60,9 +65,10 @@ class ShipmentProductLink(SQLModel, table=True):
class ProductBase(SQLModel): class ProductBase(SQLModel):
name: str name: str
unit: Unit unit: Unit
price: float price: float | None
price_kg: float | None price_kg: float | None
weight: float | None quantity: float | None
quantity_unit: str | 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")
@@ -81,12 +87,13 @@ class ProductUpdate(SQLModel):
unit: Unit | None unit: Unit | None
price: float | None price: float | None
price_kg: float | None price_kg: float | None
weight: float | None quantity: float | None
quantity_unit: str | None
productor_id: int | None productor_id: int | None
shipment_ids: list[int] | None = [] type: ProductType | None
class ProductCreate(ProductBase): class ProductCreate(ProductBase):
shipment_ids: list[int] | None = [] pass
class FormBase(SQLModel): class FormBase(SQLModel):
name: str name: str
@@ -106,7 +113,13 @@ class Form(FormBase, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
productor: Optional['Productor'] = Relationship() productor: Optional['Productor'] = Relationship()
referer: Optional['User'] = Relationship() referer: Optional['User'] = Relationship()
shipments: list["Shipment"] = Relationship(back_populates="form", cascade_delete=True) shipments: list["Shipment"] = Relationship(
back_populates="form",
cascade_delete=True,
sa_relationship_kwargs={
"order_by": "Shipment.name"
},
)
class FormUpdate(SQLModel): class FormUpdate(SQLModel):
name: str | None name: str | None
@@ -161,7 +174,13 @@ class ShipmentPublic(ShipmentBase):
class Shipment(ShipmentBase, table=True): class Shipment(ShipmentBase, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
products: list[Product] = Relationship(back_populates="shipments", link_model=ShipmentProductLink) products: list[Product] = Relationship(
back_populates="shipments",
link_model=ShipmentProductLink,
sa_relationship_kwargs={
"order_by": "Product.name"
},
)
form: Optional[Form] = Relationship(back_populates="shipments") form: Optional[Form] = Relationship(back_populates="shipments")
class ShipmentUpdate(SQLModel): class ShipmentUpdate(SQLModel):

View File

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

View File

@@ -11,9 +11,10 @@ router = APIRouter(prefix='/products')
def get_products( def get_products(
session: Session = Depends(get_session), session: Session = Depends(get_session),
names: list[str] = Query([]), names: list[str] = Query([]),
types: list[str] = Query([]),
productors: list[str] = Query([]), productors: list[str] = Query([]),
): ):
return service.get_all(session, names, productors) return service.get_all(session, names, productors, types)
@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

@@ -4,23 +4,25 @@ import src.models as models
def get_all( def get_all(
session: Session, session: Session,
names: list[str], names: list[str],
productors: list[str] productors: list[str],
types: list[str],
) -> list[models.ProductPublic]: ) -> list[models.ProductPublic]:
statement = select(models.Product) statement = select(models.Product)
if len(names) > 0: if len(names) > 0:
statement = statement.where(models.Product.name.in_(names)) statement = statement.where(models.Product.name.in_(names))
if len(productors) > 0: if len(productors) > 0:
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors)) statement = statement.join(models.Productor).where(models.Productor.name.in_(productors))
return session.exec(statement).all() if len(types) > 0:
statement = statement.where(models.Product.type.in_(types))
return session.exec(statement.order_by(models.Product.name)).all()
def get_one(session: Session, product_id: int) -> models.ProductPublic: def get_one(session: Session, product_id: int) -> models.ProductPublic:
return session.get(models.Product, product_id) return session.get(models.Product, product_id)
def create_one(session: Session, product: models.ProductCreate) -> models.ProductPublic: def create_one(session: Session, product: models.ProductCreate) -> models.ProductPublic:
shipments = session.exec(select(models.Shipment).where(models.Shipment.id.in_(product.shipment_ids))).all() product_create = product.model_dump(exclude_unset=True)
new_product = models.Product(**product_create)
product_create = product.model_dump(exclude_unset=True, exclude={'shipment_ids'})
new_product = models.Product(**product_create, shipments=shipments)
session.add(new_product) session.add(new_product)
session.commit() session.commit()
session.refresh(new_product) session.refresh(new_product)
@@ -32,13 +34,7 @@ def update_one(session: Session, id: int, product: models.ProductUpdate) -> mode
new_product = result.first() new_product = result.first()
if not new_product: if not new_product:
return None return None
product_updates = product.model_dump(exclude_unset=True)
shipments_to_add = session.exec(select(models.Shipment).where(models.Shipment.id.in_(product.shipment_ids))).all()
new_product.shipments.clear()
for add in shipments_to_add:
new_product.shipments.append(add)
product_updates = product.model_dump(exclude_unset=True, exclude={"shipment_ids"})
for key, value in product_updates.items(): for key, value in product_updates.items():
setattr(new_product, key, value) setattr(new_product, key, value)

View File

@@ -3,7 +3,7 @@ import src.models as models
def get_all(session: Session) -> list[models.ShipmentPublic]: def get_all(session: Session) -> list[models.ShipmentPublic]:
statement = select(models.Shipment) statement = select(models.Shipment)
return session.exec(statement).all() return session.exec(statement.order_by(models.Shipment.name)).all()
def get_one(session: Session, shipment_id: int) -> models.ShipmentPublic: def get_one(session: Session, shipment_id: int) -> models.ShipmentPublic:
return session.get(models.Shipment, shipment_id) return session.get(models.Shipment, shipment_id)

View File

@@ -3,7 +3,7 @@ import src.models as models
def get_all(session: Session) -> list[models.TemplatePublic]: def get_all(session: Session) -> list[models.TemplatePublic]:
statement = select(models.Template) statement = select(models.Template)
return session.exec(statement).all() return session.exec(statement.order_by(models.Template.name)).all()
def get_one(session: Session, template_id: int) -> models.TemplatePublic: def get_one(session: Session, template_id: int) -> models.TemplatePublic:
return session.get(models.Template, template_id) return session.get(models.Template, template_id)

View File

@@ -3,7 +3,7 @@ import src.models as models
def get_all(session: Session) -> list[models.UserPublic]: def get_all(session: Session) -> list[models.UserPublic]:
statement = select(models.User) statement = select(models.User)
return session.exec(statement).all() return session.exec(statement.order_by(models.User.name)).all()
def get_one(session: Session, user_id: int) -> models.UserPublic: def get_one(session: Session, user_id: int) -> models.UserPublic:
return session.get(models.User, user_id) return session.get(models.User, user_id)

View File

@@ -1,10 +1,15 @@
{ {
"product name": "product name", "product name": "product name",
"product price": "product price", "product price": "product price",
"product weight": "product weight", "product quantity": "product quantity",
"product quantity unit": "product quantity unit",
"product type": "product type", "product type": "product type",
"planned": "planned", "planned": "planned",
"reccurent": "reccurent", "planned products": "planned products",
"select products per shipment": "select products per shipment.",
"recurrent products": "recurrent products",
"your selection in this category will apply for all shipments": "your selection in this category will apply for all shipments.",
"recurrent": "recurrent",
"product price kg": "product price kg", "product price kg": "product price kg",
"product unit": "product unit", "product unit": "product unit",
"grams": "grams", "grams": "grams",
@@ -19,7 +24,8 @@
"referer": "referer", "referer": "referer",
"edit form": "edit form", "edit form": "edit form",
"form name": "form name", "form name": "form name",
"contact season": "contact season", "contract season": "contract season",
"contract season recommandation": "recommandation : <Season>-<year> (example: Winter-2025)",
"start date": "start date", "start date": "start date",
"end date": "end date", "end date": "end date",
"nothing found": "nothing found", "nothing found": "nothing found",
@@ -41,13 +47,19 @@
"productor address": "productor address", "productor address": "productor address",
"productor payment": "productor payment", "productor payment": "productor payment",
"priceKg": "priceKg", "priceKg": "priceKg",
"weight": "weight", "quantity": "quantity",
"quantity unit": "quantity unit",
"unit": "sell unit",
"price": "price", "price": "price",
"create product": "create product", "create product": "create product",
"informations": "informations", "informations": "informations",
"remove product": "remove product", "remove product": "remove product",
"edit product": "edit product", "edit product": "edit product",
"shipment name": "shipment name", "shipment name": "shipment name",
"shipments": "shipments",
"shipment": "shipment",
"there is": "there is",
"for this contract": "for this contact.",
"shipment date": "shipment date", "shipment date": "shipment date",
"remove shipment": "remove shipment", "remove shipment": "remove shipment",
"productors": "productors", "productors": "productors",
@@ -65,5 +77,11 @@
"a start date": "a start date", "a start date": "a start date",
"a end date": "a end date", "a end date": "a end date",
"a productor": "a productor", "a productor": "a productor",
"a referer": "a referer" "a referer": "a referer",
"a phone": "a phone",
"a fistname": "a fistname",
"a lastname": "a lastname",
"a email": "a email",
"submit contract": "submit contract",
"all theses informations are for contract generation, no informations is stored outside of contracts": "all theses informations are for contract generation, no informations is stored outside of contracts."
} }

View File

@@ -1,15 +1,21 @@
{ {
"product name": "nom du produit", "product name": "nom du produit",
"product price": "prix du produit", "product price": "prix du produit",
"product weight": "poids du produit", "product quantity": "quantité du produit",
"product type": "type de produit", "product type": "type de produit",
"planned": "planifié", "planned": "planifié",
"reccurent": "récurrent", "planned products": "Produits planifiés par livraison",
"select products per shipment": "Selectionnez les produits pour chaque livraison.",
"recurrent": "récurrent",
"recurrent products": "Produits récurents",
"your selection in this category will apply for all shipments": "votre selection sera appliquée pour chaque livraisons (Exemple: 6 livraisons, le produits sera comptés 6 fois : une fois par livraison).",
"product price kg": "prix du produit au Kilo", "product price kg": "prix du produit au Kilo",
"product unit": "unité de vente du produit", "product unit": "unité de vente du produit",
"grams": "grammes", "grams": "grammes",
"kilo": "kilo", "kilo": "kilo",
"piece": "pièce", "piece": "pièce",
"in": "en",
"enter quantity": "entrez la quantitée",
"filter by season": "filtrer par saisons", "filter by season": "filtrer par saisons",
"name": "nom", "name": "nom",
"season": "saison", "season": "saison",
@@ -19,7 +25,8 @@
"referer": "référent·e", "referer": "référent·e",
"edit form": "modifier le formulaire de contrat", "edit form": "modifier le formulaire de contrat",
"form name": "nom du formulaire de contrat", "form name": "nom du formulaire de contrat",
"contact season": "saison du contrat", "contract season": "saison du contrat",
"contract season recommandation": "recommandation : <Saison>-<année> (Exemple: Hiver-2025)",
"start date": "date de début", "start date": "date de début",
"end date": "date de fin", "end date": "date de fin",
"nothing found": "rien à afficher", "nothing found": "rien à afficher",
@@ -35,13 +42,15 @@
"address": "adresse", "address": "adresse",
"payment": "ordre du chèque", "payment": "ordre du chèque",
"type": "type", "type": "type",
"create productor": "créer le producteur·troce", "create productor": "créer le producteur·trice",
"productor name": "nom du producteur·trice", "productor name": "nom du producteur·trice",
"productor type": "type du producteur·trice", "productor type": "type du producteur·trice",
"productor address": "adresse du producteur·trice", "productor address": "adresse du producteur·trice",
"productor payment": "ordre du chèque du producteur·trice", "productor payment": "ordre du chèque du producteur·trice",
"priceKg": "prix au kilo", "priceKg": "prix au kilo",
"weight": "poids", "quantity": "quantité",
"quantity unit": "unité de quantité",
"unit": "Unité de vente",
"price": "prix", "price": "prix",
"create product": "créer le produit", "create product": "créer le produit",
"informations": "informations", "informations": "informations",
@@ -49,6 +58,10 @@
"edit product": "modifier le produit", "edit product": "modifier le produit",
"shipment name": "nom de la livraison", "shipment name": "nom de la livraison",
"shipment date": "date de la livraison", "shipment date": "date de la livraison",
"shipments": "livraisons",
"shipment": "livraison",
"there is": "il y a",
"for this contract": "pour ce contrat.",
"remove shipment": "supprimer la livraison", "remove shipment": "supprimer la livraison",
"productors": "producteur·trices", "productors": "producteur·trices",
"products": "produits", "products": "produits",
@@ -66,5 +79,11 @@
"a start date": "une date de début", "a start date": "une date de début",
"a end date": "une date de fin", "a end date": "une date de fin",
"a productor": "un(e) producteur·trice", "a productor": "un(e) producteur·trice",
"a referer": "un référent·e" "a referer": "un référent·e",
"a phone": "un numéro de téléphone",
"a fistname": "un prénom",
"a lastname": "un nom",
"a email": "une adresse email",
"submit contract": "Envoyer le contrat",
"all theses informations are for contract generation, no informations is stored outside of contracts": "ces informations sont nécéssaires pour la génération de contrat, aucune information personnelle n'est gardée ailleurs que dans les contrats générés."
} }

View File

@@ -67,7 +67,7 @@ export default function FormModal({
return ( return (
<Modal <Modal
size="50%" w={{base: "100%", md: "80%", lg: "50%"}}
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
title={currentForm ? t("edit form") : t('create form')} title={currentForm ? t("edit form") : t('create form')}
@@ -80,24 +80,27 @@ export default function FormModal({
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <TextInput
label={t("contact season", {capfirst: true})} label={t("contract season", {capfirst: true})}
placeholder={t("contact season", {capfirst: true})} placeholder={t("contract season", {capfirst: true})}
description={t("contract season recommandation", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('season')} {...form.getInputProps('season')}
/> />
<DatePickerInput <Group grow>
label={t("start date", {capfirst: true})} <DatePickerInput
placeholder={t("start date", {capfirst: true})} label={t("start date", {capfirst: true})}
withAsterisk placeholder={t("start date", {capfirst: true})}
{...form.getInputProps('start')} withAsterisk
/> {...form.getInputProps('start')}
<DatePickerInput />
label={t("end date", {capfirst: true})} <DatePickerInput
placeholder={t("end date", {capfirst: true})} label={t("end date", {capfirst: true})}
withAsterisk placeholder={t("end date", {capfirst: true})}
{...form.getInputProps('end')} withAsterisk
/> {...form.getInputProps('end')}
/>
</Group>
<Select <Select
label={t("referer", {capfirst: true})} label={t("referer", {capfirst: true})}
placeholder={t("referer", {capfirst: true})} placeholder={t("referer", {capfirst: true})}
@@ -127,7 +130,6 @@ export default function FormModal({
aria-label={t("cancel", {capfirst: true})} aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>} leftSection={<IconCancel/>}
onClick={() => { onClick={() => {
form.reset();
form.clearErrors(); form.clearErrors();
onClose(); onClose();
}} }}
@@ -139,7 +141,6 @@ export default function FormModal({
form.validate(); form.validate();
if (form.isValid()) { if (form.isValid()) {
handleSubmit(form.getValues(), currentForm?.id) handleSubmit(form.getValues(), currentForm?.id)
form.reset();
} }
}} }}
>{currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}</Button> >{currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}</Button>

View File

@@ -1,5 +1,5 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { deleteForm} from "@/services/api"; import { deleteForm} from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
@@ -12,6 +12,7 @@ export type FormRowProps = {
export default function FormRow({ export default function FormRow({
form, form,
}: FormRowProps) { }: FormRowProps) {
const [searchParams, _] = useSearchParams();
const deleteMutation = deleteForm(); const deleteMutation = deleteForm();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -30,7 +31,7 @@ export default function FormRow({
mr="5" mr="5"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/forms/${form.id}/edit`); navigate(`/dashboard/forms/${form.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconEdit/> <IconEdit/>

View File

@@ -6,7 +6,7 @@ export function Navbar() {
return ( return (
<nav> <nav>
<NavLink to="/">{t("home", {capfirst: true})}</NavLink> <NavLink to="/">{t("home", {capfirst: true})}</NavLink>
<NavLink to="/dashboard">{t("dashboard", {capfirst: true})}</NavLink> <NavLink to="/dashboard/productors">{t("dashboard", {capfirst: true})}</NavLink>
</nav> </nav>
); );
} }

View File

@@ -33,6 +33,7 @@ export default function ProductorsFilter({
onFilterChange(values, 'names') onFilterChange(values, 'names')
}} }}
clearable clearable
searchable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by type", {capfirst: true})} aria-label={t("filter by type", {capfirst: true})}
@@ -43,6 +44,7 @@ export default function ProductorsFilter({
onFilterChange(values, 'types') onFilterChange(values, 'types')
}} }}
clearable clearable
searchable
/> />
</Group> </Group>
); );

View File

@@ -45,7 +45,7 @@ export function ProductorModal({
return ( return (
<Modal <Modal
size="50%" w={{base: "100%", md: "80%", lg: "50%"}}
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
title={t("create productor", {capfirst: true})} title={t("create productor", {capfirst: true})}
@@ -87,7 +87,6 @@ export function ProductorModal({
aria-label={t("cancel", {capfirst: true})} aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>} leftSection={<IconCancel/>}
onClick={() => { onClick={() => {
form.reset();
form.clearErrors(); form.clearErrors();
onClose(); onClose();
}} }}
@@ -99,7 +98,6 @@ export function ProductorModal({
form.validate(); form.validate();
if (form.isValid()) { if (form.isValid()) {
handleSubmit(form.getValues(), currentProductor?.id) handleSubmit(form.getValues(), currentProductor?.id)
form.reset();
} }
}} }}
>{currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}</Button> >{currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}</Button>

View File

@@ -3,7 +3,7 @@ import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import type { Productor } from "@/services/resources/productors"; import type { Productor } from "@/services/resources/productors";
import { deleteProductor } from "@/services/api"; import { deleteProductor } from "@/services/api";
import { useNavigate } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
export type ProductorRowProps = { export type ProductorRowProps = {
productor: Productor; productor: Productor;
@@ -12,6 +12,7 @@ export type ProductorRowProps = {
export default function ProductorRow({ export default function ProductorRow({
productor, productor,
}: ProductorRowProps) { }: ProductorRowProps) {
const [searchParams, _] = useSearchParams();
const deleteMutation = deleteProductor(); const deleteMutation = deleteProductor();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -28,7 +29,7 @@ export default function ProductorRow({
mr="5" mr="5"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/productors/${productor.id}/edit`); navigate(`/dashboard/productors/${productor.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconEdit/> <IconEdit/>

View File

@@ -33,6 +33,7 @@ export default function ProductsFilters({
onFilterChange(values, 'names') onFilterChange(values, 'names')
}} }}
clearable clearable
searchable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by productor", {capfirst: true})} aria-label={t("filter by productor", {capfirst: true})}
@@ -43,6 +44,7 @@ export default function ProductsFilters({
onFilterChange(values, 'productors') onFilterChange(values, 'productors')
}} }}
clearable clearable
searchable
/> />
</Group> </Group>
); );

View File

@@ -0,0 +1,44 @@
import { t } from "@/config/i18n";
import { ProductUnit, type Product } from "@/services/resources/products";
import type { Shipment } from "@/services/resources/shipments";
import { Group, NumberInput } from "@mantine/core";
import type { UseFormReturnType } from "@mantine/form";
export type ProductFormProps = {
inputForm: UseFormReturnType<Record<string, string | number>>;
product: Product;
shipment?: Shipment;
}
export function ProductForm({
inputForm,
product,
shipment,
}: ProductFormProps) {
return (
<Group mb="sm" grow>
<NumberInput
label={
`${product.name}
${product.quantity || ""}${product.quantity ? product.quantity_unit : ""}
${
product.price ?
Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(product.price) :
product.price_kg && Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(product.price_kg)
}
${product.price ? `/ ${t(ProductUnit[product.unit])}` : "/ kg"}`
}
description={`${t("enter quantity", {capfirst: true})} ${t('in')} ${t(ProductUnit[product.unit])}`}
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", {capfirst: true})} ${t('in')} ${t(ProductUnit[product.unit])}`}
{...inputForm.getInputProps(shipment ? `planned-${shipment.id}-${product.id}` : `recurrent-${product.id}`)}
/>
</Group>
);
}

View File

@@ -1,8 +1,8 @@
import { Button, Group, Modal, NumberInput, Select, TextInput, Title, type ModalBaseProps } from "@mantine/core"; import { Button, Group, Modal, NumberInput, Pill, Select, TextInput, Title, Tooltip, type ModalBaseProps } from "@mantine/core";
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, IconInfoCircle } from "@tabler/icons-react";
import { productToProductInputs, type Product, type ProductInputs } from "@/services/resources/products"; import { ProductQuantityUnit, productToProductInputs, ProductUnit, 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";
@@ -24,7 +24,8 @@ export function ProductModal({
unit: null, unit: null,
price: null, price: null,
price_kg: null, price_kg: null,
weight: null, quantity: null,
quantity_unit: null,
type: null, type: null,
productor_id: null, productor_id: null,
}, },
@@ -33,8 +34,10 @@ export function ProductModal({
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null, !value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
unit: (value) => unit: (value) =>
!value ? `${t("unit", {capfirst: true})} ${t('is required')}` : null, !value ? `${t("unit", {capfirst: true})} ${t('is required')}` : null,
price: (value) => price: (value, values) =>
!value ? `${t("price", {capfirst: true})} ${t('is required')}` : null, !value && !values.price_kg ? `${t("price or price_kg", {capfirst: true})} ${t('is required')}` : null,
price_kg: (value, values) =>
!value && !values.price ? `${t("price or price_kg", {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) =>
@@ -54,70 +57,85 @@ export function ProductModal({
return ( return (
<Modal <Modal
size="50%" w={{base: "100%", md: "80%", lg: "50%"}}
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
title={t("create product", {capfirst: true})} title={t("create product", {capfirst: true})}
> >
<Title order={4}>{t("informations", {capfirst: true})}</Title> <Title order={4}>{t("informations", {capfirst: true})}</Title>
<TextInput
label={t("product name", {capfirst: true})}
placeholder={t("product name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<Select <Select
label={t("product type", {capfirst: true})}
placeholder={t("product type", {capfirst: true})}
radius="sm"
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})}
radius="sm"
withAsterisk
{...form.getInputProps('price')}
/>
<NumberInput
label={t("product priceKg", {capfirst: true})}
placeholder={t("product priceKg", {capfirst: true})}
radius="sm"
{...form.getInputProps('price_kg')}
/>
<NumberInput
label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})}
radius="sm"
{...form.getInputProps('weight', {capfirst: true})}
/>
<Select
label={t("productor", {capfirst: true})} label={t("productor", {capfirst: true})}
placeholder={t("productor")} placeholder={t("productor")}
nothingFoundMessage={t("nothing found", {capfirst: true})} nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk withAsterisk
clearable clearable
allowDeselect
searchable searchable
data={productorsSelect || []} data={productorsSelect || []}
{...form.getInputProps('productor_id')} {...form.getInputProps('productor_id')}
/> />
<Group grow>
<TextInput
label={t("product name", {capfirst: true})}
placeholder={t("product name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<Select
label={t("product type", {capfirst: true})}
placeholder={t("product type", {capfirst: true})}
radius="sm"
withAsterisk
searchable
clearable
data={[
{value: "1", label: t("planned")},
{value: "2", label: t("recurrent")}
]}
{...form.getInputProps('type')}
/>
</Group>
<Select
label={t("product unit", {capfirst: true})}
placeholder={t("product unit", {capfirst: true})}
description={t("the product unit will be assigned to the quantity requested in the form", { capfirst: true})}
radius="sm"
withAsterisk
searchable
clearable
data={Object.entries(ProductUnit).map(([key, value]) => ({value: key, label: t(value)}))}
{...form.getInputProps('unit')}
/>
<Group grow>
<NumberInput
label={t("product price", {capfirst: true})}
placeholder={t("product price", {capfirst: true})}
radius="sm"
{...form.getInputProps('price')}
/>
<NumberInput
label={t("product price kg", {capfirst: true})}
placeholder={t("product price kg", {capfirst: true})}
radius="sm"
{...form.getInputProps('price_kg')}
/>
</Group>
<Group grow>
<NumberInput
label={t("product quantity", {capfirst: true})}
placeholder={t("product quantity", {capfirst: true})}
radius="sm"
{...form.getInputProps('quantity', {capfirst: true})}
/>
<Select
label={t("product quantity unit", {capfirst: true})}
placeholder={t("product quantity unit", {capfirst: true})}
radius="sm"
data={Object.entries(ProductQuantityUnit).map(([key, value]) => ({value: key, label: t(value)}))}
{...form.getInputProps('quantity_unit', {capfirst: true})}
/>
</Group>
<Group mt="sm" justify="space-between"> <Group mt="sm" justify="space-between">
<Button <Button
variant="filled" variant="filled"
@@ -125,7 +143,6 @@ export function ProductModal({
aria-label={t("cancel", {capfirst: true})} aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>} leftSection={<IconCancel/>}
onClick={() => { onClick={() => {
form.reset();
form.clearErrors(); form.clearErrors();
onClose(); onClose();
}} }}
@@ -138,7 +155,6 @@ export function ProductModal({
console.log(form.isValid(), form.getValues()) console.log(form.isValid(), form.getValues())
if (form.isValid()) { if (form.isValid()) {
handleSubmit(form.getValues(), currentProduct?.id) handleSubmit(form.getValues(), currentProduct?.id)
form.reset();
} }
}} }}
>{currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}</Button> >{currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}</Button>

View File

@@ -1,10 +1,9 @@
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 { ProductType, ProductUnit, type Product, type ProductInputs } from "@/services/resources/products"; import { ProductType, ProductUnit, type Product } from "@/services/resources/products";
import { ProductModal } from "@/components/Products/Modal"; import { deleteProduct } from "@/services/api";
import { deleteProduct, getProduct } from "@/services/api"; import { useNavigate, useSearchParams } from "react-router";
import { useNavigate } from "react-router";
export type ProductRowProps = { export type ProductRowProps = {
product: Product; product: Product;
@@ -13,17 +12,19 @@ export type ProductRowProps = {
export default function ProductRow({ export default function ProductRow({
product, product,
}: ProductRowProps) { }: ProductRowProps) {
const [searchParams, _] = useSearchParams();
const deleteMutation = deleteProduct(); const deleteMutation = deleteProduct();
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<Table.Tr key={product.id}> <Table.Tr key={product.id}>
<Table.Td>{product.name}</Table.Td> <Table.Td>{product.name}</Table.Td>
<Table.Td>{ProductType[product.type]}</Table.Td> <Table.Td>{t(ProductType[product.type])}</Table.Td>
<Table.Td>{product.price}</Table.Td> <Table.Td>{product.price}</Table.Td>
<Table.Td>{product.price_kg}</Table.Td> <Table.Td>{product.price_kg}</Table.Td>
<Table.Td>{product.weight}</Table.Td> <Table.Td>{product.quantity}</Table.Td>
<Table.Td>{ProductUnit[product.unit]}</Table.Td> <Table.Td>{product.quantity_unit}</Table.Td>
<Table.Td>{t(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
@@ -31,7 +32,7 @@ export default function ProductRow({
mr="5" mr="5"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/products/${product.id}/edit`); navigate(`/dashboard/products/${product.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconEdit/> <IconEdit/>

View File

@@ -1,62 +1,57 @@
import { ActionIcon, Group, TextInput, Tooltip } from "@mantine/core"; import { Accordion, Group, Text } from "@mantine/core";
import { DatePickerInput } from "@mantine/dates"; import type { Shipment } from "@/services/resources/shipments";
import { IconX } from "@tabler/icons-react"; import { ProductForm } from "@/components/Products/Form";
import { t } from "@/config/i18n"; import type { UseFormReturnType } from "@mantine/form";
import type { ShipmentInputs } from "@/services/resources/shipments"; import { useMemo } from "react";
import { computePrices } from "@/pages/Contract";
export type ShipmentFormProps = { export type ShipmentFormProps = {
inputForm: UseFormReturnType<Record<string, string | number>>;
shipment: Shipment;
index: number; index: number;
setShipmentElement: (index: number, shipment: ShipmentInputs) => void;
deleteShipmentElement: (index: number) => void;
shipment: ShipmentInputs;
} }
export default function ShipmentForm({ export default function ShipmentForm({
index, shipment,
setShipmentElement, index,
deleteShipmentElement, inputForm,
shipment
}: ShipmentFormProps) { }: ShipmentFormProps) {
return ( const shipmentPrice = useMemo(() => {
<Group justify="space-between" key={`shipment_${index}`}> const values = Object
<Group grow maw="80%"> .entries(inputForm.getValues())
<TextInput .filter(([key]) =>
label={t("shipment name", {capfirst: true})} key.includes("planned") &&
placeholder={t("shipment name", {capfirst: true})} key.split("-")[1] === String(shipment.id)
radius="sm" );
withAsterisk return computePrices(values, shipment.products);
value={shipment.name || ""} }, [inputForm, shipment.products]);
onChange={(event) => {
const value = event.target.value;
setShipmentElement(index, {...shipment, name: value})
}}
/>
<DatePickerInput
label={t("shipment date", {capfirst: true})}
placeholder={t("shipment date", {capfirst: true})}
radius="sm"
withAsterisk
value={shipment.date || null}
onChange={(event) => {
const value = event || "";
setShipmentElement(index, {...shipment, date: value})
}}
/>
</Group>
<Tooltip label={t("remove shipment", {capfirst: true})}>
<ActionIcon
flex={{base: "1", md: "0"}}
style={{alignSelf: "flex-end"}}
color="red"
aria-label={t("remove shipment", {capfirst: true})}
onClick={() => {
deleteShipmentElement(index)
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Group>
) return (
<Accordion.Item value={String(index)}>
<Accordion.Control>
<Group justify="space-between">
<Text>{shipment.name}</Text>
<Text>{
Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(shipmentPrice)
}</Text>
<Text mr="lg">{shipment.date}</Text>
</Group>
</Accordion.Control>
<Accordion.Panel>
{
shipment?.products.map((product) => (
<ProductForm
key={product.id}
product={product}
shipment={shipment}
inputForm={inputForm}
/>
))
}
</Accordion.Panel>
</Accordion.Item>
);
} }

View File

@@ -5,7 +5,7 @@ import { IconCancel } from "@tabler/icons-react";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { shipmentToShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments"; import { shipmentToShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
import { getForms, getProducts } from "@/services/api"; import { getForms, getProductors, getProducts } from "@/services/api";
export type ShipmentModalProps = ModalBaseProps & { export type ShipmentModalProps = ModalBaseProps & {
currentShipment?: Shipment; currentShipment?: Shipment;
@@ -42,18 +42,28 @@ export default function ShipmentModal({
}, [currentShipment]); }, [currentShipment]);
const { data: allForms } = getForms(); const { data: allForms } = getForms();
const { data: allProducts } = getProducts(); const { data: allProducts } = getProducts(new URLSearchParams("types=1"));
const { data: allProductors } = getProductors()
const formsSelect = useMemo(() => { const formsSelect = useMemo(() => {
return allForms?.map(form => ({value: String(form.id), label: `${form.name} ${form.season}`})) return allForms?.map(form => ({value: String(form.id), label: `${form.name} ${form.season}`}))
}, [allForms]); }, [allForms]);
const productsSelect = useMemo(() => { const productsSelect = useMemo(() => {
return allProducts?.map(product => ({value: String(product.id), label: `${product.name}`})) if (!allProducts)
}, [allProducts]); return;
return allProductors?.map(productor => {
return {
group: productor.name,
items: allProducts
.filter((product) => product.productor.id === productor.id)
.map((product) => ({value: String(product.id), label: `${product.name}`}))
}
});
}, [allProducts, form]);
return ( return (
<Modal <Modal
size="50%" w={{base: "100%", md: "80%", lg: "50%"}}
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
title={currentShipment ? t("edit shipment") : t('create shipment')} title={currentShipment ? t("edit shipment") : t('create shipment')}
@@ -83,8 +93,9 @@ export default function ShipmentModal({
<MultiSelect <MultiSelect
label={t("shipment products", {capfirst: true})} label={t("shipment products", {capfirst: true})}
placeholder={t("shipment products", {capfirst: true})} placeholder={t("shipment products", {capfirst: true})}
data={productsSelect} data={productsSelect || []}
clearable clearable
searchable
{...form.getInputProps('product_ids')} {...form.getInputProps('product_ids')}
/> />
<Group mt="sm" justify="space-between"> <Group mt="sm" justify="space-between">
@@ -105,7 +116,6 @@ export default function ShipmentModal({
form.validate(); form.validate();
if (form.isValid()) { if (form.isValid()) {
handleSubmit(form.getValues(), currentShipment?.id) handleSubmit(form.getValues(), currentShipment?.id)
// form.reset();
} }
}} }}
>{currentShipment ? t("edit shipment", {capfirst: true}) : t('create shipment', {capfirst: true})}</Button> >{currentShipment ? t("edit shipment", {capfirst: true}) : t('create shipment', {capfirst: true})}</Button>

View File

@@ -1,5 +1,5 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { deleteShipment} from "@/services/api"; import { deleteShipment} from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
@@ -12,6 +12,7 @@ export type ShipmentRowProps = {
export default function ShipmentRow({ export default function ShipmentRow({
shipment, shipment,
}: ShipmentRowProps) { }: ShipmentRowProps) {
const [searchParams, _] = useSearchParams();
const deleteMutation = deleteShipment(); const deleteMutation = deleteShipment();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -27,7 +28,7 @@ export default function ShipmentRow({
mr="5" mr="5"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/shipments/${shipment.id}/edit`); navigate(`/dashboard/shipments/${shipment.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconEdit/> <IconEdit/>

View File

@@ -37,7 +37,7 @@ export function UserModal({
return ( return (
<Modal <Modal
size="50%" w={{base: "100%", md: "80%", lg: "50%"}}
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
title={t("create user", {capfirst: true})} title={t("create user", {capfirst: true})}
@@ -64,7 +64,6 @@ export function UserModal({
aria-label={t("cancel", {capfirst: true})} aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>} leftSection={<IconCancel/>}
onClick={() => { onClick={() => {
form.reset();
form.clearErrors(); form.clearErrors();
onClose(); onClose();
}} }}
@@ -77,7 +76,6 @@ export function UserModal({
console.log(form.isValid(), form.getValues()) console.log(form.isValid(), form.getValues())
if (form.isValid()) { if (form.isValid()) {
handleSubmit(form.getValues(), currentUser?.id) handleSubmit(form.getValues(), currentUser?.id)
form.reset();
} }
}} }}
>{currentUser ? t("edit user", {capfirst: true}) : t('create user', {capfirst: true})}</Button> >{currentUser ? t("edit user", {capfirst: true}) : t('create user', {capfirst: true})}</Button>

View File

@@ -4,7 +4,7 @@ import { IconEdit, IconX } from "@tabler/icons-react";
import { type User, type UserInputs } from "@/services/resources/users"; import { type User, type UserInputs } from "@/services/resources/users";
import { UserModal } from "@/components/Users/Modal"; import { UserModal } from "@/components/Users/Modal";
import { deleteUser, getUser } from "@/services/api"; import { deleteUser, getUser } from "@/services/api";
import { useNavigate } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
export type UserRowProps = { export type UserRowProps = {
user: User; user: User;
@@ -13,6 +13,7 @@ export type UserRowProps = {
export default function UserRow({ export default function UserRow({
user, user,
}: UserRowProps) { }: UserRowProps) {
const [searchParams, _] = useSearchParams();
const deleteMutation = deleteUser(); const deleteMutation = deleteUser();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -27,7 +28,7 @@ export default function UserRow({
mr="5" mr="5"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/users/${user.id}/edit`); navigate(`/dashboard/users/${user.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconEdit/> <IconEdit/>

View File

@@ -15,11 +15,10 @@ const resources = {
}; };
i18next i18next
.use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources: resources, resources: resources,
fallbackLng: "en", fallbackLng: "fr",
debug: Config.debug, debug: Config.debug,
detection: { detection: {
caches: [], caches: [],

View File

@@ -1,52 +1,193 @@
import { ProductForm } from "@/components/Products/Form";
import ShipmentForm from "@/components/Shipments/Form";
import { t } from "@/config/i18n";
import { getForm } from "@/services/api"; import { getForm } from "@/services/api";
import { Group, Loader, NumberInput, Stack, Text, Title } from "@mantine/core"; import { type Product } from "@/services/resources/products";
import { Accordion, Button, Group, List, Loader, Overlay, Stack, Text, TextInput, Title } from "@mantine/core";
import { useForm } from "@mantine/form";
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { useParams } from "react-router"; import { useParams } from "react-router";
export function computePrices(values: [string, any][], products: Product[], nbShipment?: number) {
return values.reduce((prev, [key, value]) => {
const keyArray = key.split("-");
const productId = Number(keyArray[keyArray.length - 1]);
const product = products.find((product) => product.id === productId);
if (!product) {
return 0;
}
const isRecurent = key.includes("recurrent") && nbShipment;
const productPrice = Number(product.price || product.price_kg);
const productQuantityUnit = product.unit === "2" ? 1 : 1000;
const productQuantity = Number(product.price ? value : value / productQuantityUnit);
return(prev + productPrice * productQuantity * (isRecurent ? nbShipment : 1));
}, 0);
}
export function Contract() { export function Contract() {
const { id } = useParams(); const { id } = useParams();
const { data: form } = getForm(Number(id), {enabled: !!id}) const { data: form } = getForm(Number(id), {enabled: !!id});
const inputForm = useForm<Record<string, number | string>>({
validate: {
firstname: (value) => !value ? `${t("a firstname", {capfirst: true})} ${t("is required")}` : null,
lastname: (value) => !value ? `${t("a lastname", {capfirst: true})} ${t("is required")}` : null,
email: (value) => !value ? `${t("a email", {capfirst: true})} ${t("is required")}` : null,
phone: (value) => !value ? `${t("a phone", {capfirst: true})} ${t("is required")}` : null,
}
});
const productsRecurent = useMemo(() => { const productsRecurent = useMemo(() => {
console.log(form)
return form?.productor?.products.filter((el) => el.type === "2") return form?.productor?.products.filter((el) => el.type === "2")
}, [form]) }, [form]);
const shipments = useMemo(() => { const shipments = useMemo(() => {
return form?.shipments return form?.shipments;
}, [form]);
const allProducts = useMemo(() => {
return form?.productor?.products;
}, [form]) }, [form])
const price = useMemo(() => {
const values = Object.entries(inputForm.getValues());
return computePrices(values, allProducts, form?.shipments.length);
}, [inputForm, allProducts, form?.shipments]);
if (!form) if (!form)
return <Loader/> return <Loader/>;
return ( return (
<Stack> <Stack w={{base: "100%", md: "80%", lg: "50%"}}>
<Title>{form.name}</Title> <Title order={2}>{form.name}</Title>
<Title order={3}>{t("informations", {capfirst: true})}</Title>
<Text size="sm">
{t("all theses informations are for contract generation, no informations is stored outside of contracts", {capfirst: true})}
</Text>
<Group grow>
<TextInput
label={t("firstname", {capfirst: true})}
placeholder={t("firstname", {capfirst: true})}
radius="sm"
withAsterisk
required
leftSection={<IconUser/>}
{...inputForm.getInputProps('firstname')}
/>
<TextInput
label={t("lastname", {capfirst: true})}
placeholder={t("lastname", {capfirst: true})}
radius="sm"
withAsterisk
required
leftSection={<IconUser/>}
{...inputForm.getInputProps('lastname')}
/>
</Group>
<Group grow>
<TextInput
label={t("email", {capfirst: true})}
placeholder={t("email", {capfirst: true})}
radius="sm"
withAsterisk
required
leftSection={<IconMail/>}
{...inputForm.getInputProps('email')}
/>
<TextInput
label={t("phone", {capfirst: true})}
placeholder={t("phone", {capfirst: true})}
radius="sm"
withAsterisk
required
leftSection={<IconPhone/>}
{...inputForm.getInputProps('phone')}
/>
</Group>
<Title order={3}>{t('shipments', {capfirst: true})}</Title>
<Text>{`${t("there is", {capfirst: true})} ${shipments.length} ${shipments.length > 1 ? t("shipments") : t("shipment")} ${t("for this contract")}`}</Text>
<List>
{
shipments.map(shipment => (
<List.Item key={shipment.id}>{`${shipment.name} :
${
new Date(shipment.date).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
}`}
</List.Item>
))
}
</List>
{ {
productsRecurent.map((el) => ( productsRecurent.length > 0 ?
<Group> <>
<Text>{el.name}</Text> <Title order={3}>{t('recurrent products', {capfirst: true})}</Title>
<NumberInput/> <Text size="sm">{t('your selection in this category will apply for all shipments', {capfirst: true})}</Text>
</Group> {
)) productsRecurent.map((product) => (
<ProductForm
key={product.id}
product={product}
inputForm={inputForm}
/>
))
}
</> :
null
} }
{ {
shipments.map((shipment) => ( shipments.some(shipment => shipment.products.length > 0) ?
<> <>
<Text>{shipment.name}</Text> <Title order={3}>{t("planned products")}</Title>
{ <Text>{t("select products per shipment")}</Text>
shipment?.products.map((product) => ( <Accordion defaultValue={"0"}>
<Group> {
<Text>{product.name}</Text> shipments.map((shipment, index) => (
<NumberInput/> <ShipmentForm
</Group> shipment={shipment}
index={index}
)) inputForm={inputForm}
} key={shipment.id}
</> />
))
)) }
</Accordion>
</> :
null
} }
<Overlay
bg={"lightGray"}
h="10vh"
p="sm"
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
position: "sticky",
bottom: "0px",
}}
>
<Text>{
t("total", {capfirst: true})} : {Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(price)}
</Text>
<Button
aria-label={t('submit contract')}
onClick={() => {
inputForm.validate();
console.log(inputForm.getValues())
}}
>
{t('submit contract')}
</Button>
</Overlay>
</Stack> </Stack>
) )
} }

View File

@@ -25,7 +25,7 @@ export function Forms() {
}, [location, isEdit]) }, [location, isEdit])
const closeModal = () => { const closeModal = () => {
navigate("/dashboard/forms"); navigate(`/dashboard/forms${searchParams ? `?${searchParams.toString()}` : ""}`);
}; };
const { isPending, data } = getForms(searchParams); const { isPending, data } = getForms(searchParams);
@@ -102,7 +102,7 @@ export function Forms() {
<ActionIcon <ActionIcon
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/forms/create`); navigate(`/dashboard/forms/create${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconPlus/> <IconPlus/>

View File

@@ -5,7 +5,7 @@ import { IconPlus } from "@tabler/icons-react";
import ProductorRow from "@/components/Productors/Row"; import ProductorRow from "@/components/Productors/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { ProductorModal } from "@/components/Productors/Modal"; import { ProductorModal } from "@/components/Productors/Modal";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo } from "react";
import type { Productor, ProductorInputs } from "@/services/resources/productors"; import type { Productor, ProductorInputs } from "@/services/resources/productors";
import ProductorsFilters from "@/components/Productors/Filter"; import ProductorsFilters from "@/components/Productors/Filter";
@@ -25,7 +25,7 @@ export default function Productors() {
}, [location, isEdit]) }, [location, isEdit])
const closeModal = () => { const closeModal = () => {
navigate("/dashboard/productors"); navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
}; };
const { data: productors, isPending } = getProductors(searchParams); const { data: productors, isPending } = getProductors(searchParams);
@@ -85,7 +85,7 @@ export default function Productors() {
<ActionIcon <ActionIcon
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/productors/create`); navigate(`/dashboard/productors/create${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconPlus/> <IconPlus/>

View File

@@ -25,7 +25,7 @@ export default function Products() {
}, [location, isEdit]) }, [location, isEdit])
const closeModal = () => { const closeModal = () => {
navigate("/dashboard/products"); navigate(`/dashboard/products${searchParams ? `?${searchParams.toString()}` : ""}`);
}; };
const { data: products, isPending } = getProducts(searchParams); const { data: products, isPending } = getProducts(searchParams);
@@ -38,7 +38,7 @@ export default function Products() {
}, [allProducts]) }, [allProducts])
const productors = useMemo(() => { const productors = useMemo(() => {
return allProducts?.map((form: Product) => (form.productor.name)) return allProducts?.map((product: Product) => (product.productor.name))
.filter((productor, index, array) => array.indexOf(productor) === index) .filter((productor, index, array) => array.indexOf(productor) === index)
}, [allProducts]) }, [allProducts])
@@ -55,7 +55,7 @@ export default function Products() {
return; return;
await editProductMutation.mutateAsync({ await editProductMutation.mutateAsync({
id: id, id: id,
product: product product: productCreateFromProductInputs(product)
}); });
closeModal(); closeModal();
}, []); }, []);
@@ -83,7 +83,7 @@ export default function Products() {
<ActionIcon <ActionIcon
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/products/create`); navigate(`/dashboard/products/create${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconPlus/> <IconPlus/>
@@ -115,7 +115,8 @@ export default function Products() {
<Table.Th>{t("type", {capfirst: true})}</Table.Th> <Table.Th>{t("type", {capfirst: true})}</Table.Th>
<Table.Th>{t("price", {capfirst: true})}</Table.Th> <Table.Th>{t("price", {capfirst: true})}</Table.Th>
<Table.Th>{t("priceKg", {capfirst: true})}</Table.Th> <Table.Th>{t("priceKg", {capfirst: true})}</Table.Th>
<Table.Th>{t("weight", {capfirst: true})}</Table.Th> <Table.Th>{t("quantity", {capfirst: true})}</Table.Th>
<Table.Th>{t("quantity unit", {capfirst: true})}</Table.Th>
<Table.Th>{t("unit", {capfirst: true})}</Table.Th> <Table.Th>{t("unit", {capfirst: true})}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th> <Table.Th>{t("actions", {capfirst: true})}</Table.Th>
</Table.Tr> </Table.Tr>

View File

@@ -25,7 +25,7 @@ export default function Shipments() {
}, [location, isEdit]) }, [location, isEdit])
const closeModal = () => { const closeModal = () => {
navigate("/dashboard/shipments"); navigate(`/dashboard/shipments${searchParams ? `?${searchParams.toString()}` : ""}`);
}; };
const { data: shipments, isPending } = getShipments(searchParams); const { data: shipments, isPending } = getShipments(searchParams);
@@ -78,7 +78,7 @@ export default function Shipments() {
<ActionIcon <ActionIcon
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/shipments/create`); navigate(`/dashboard/shipments/create${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconPlus/> <IconPlus/>

View File

@@ -25,7 +25,7 @@ export default function Users() {
}, [location, isEdit]) }, [location, isEdit])
const closeModal = () => { const closeModal = () => {
navigate("/dashboard/users"); navigate(`/dashboard/users${searchParams ? `?${searchParams.toString()}` : ""}`);
}; };
const {data: users, isPending} = getUsers(searchParams); const {data: users, isPending} = getUsers(searchParams);
@@ -79,7 +79,7 @@ export default function Users() {
<ActionIcon <ActionIcon
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/users/create`); navigate(`/dashboard/users/create${searchParams ? `?${searchParams.toString()}` : ""}`);
}} }}
> >
<IconPlus/> <IconPlus/>

View File

@@ -1,29 +1,37 @@
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 = [ type ProductTypeKey = "1" | "2";
"none", type ProductUnitKey = "1" | "2" | "3";
t("planned"),
t("reccurent"),
];
export const ProductUnit = [ export const ProductType = {
"none", "1": "planned",
t("grams"), "2": "recurrent",
t("kilo"), };
t("piece"),
]; export const ProductUnit = {
"1": "grams",
"2": "kilo",
"3": "piece",
};
export const ProductQuantityUnit = {
"ml": "mililiter",
"L": "liter",
"g": "grams",
"kg": "kilo"
}
export type Product = { export type Product = {
id: number; id: number;
productor: Productor; productor: Productor;
name: string; name: string;
unit: string; unit: ProductUnitKey;
price: number; price: number | null;
price_kg: number | null; price_kg: number | null;
weight: number; quantity: number | null;
type: string; quantity_unit: string | null;
type: ProductTypeKey;
shipments: Shipment[]; shipments: Shipment[];
} }
@@ -31,9 +39,10 @@ export type ProductCreate = {
productor_id: number; productor_id: number;
name: string; name: string;
unit: string; unit: string;
price: number; price: number | null;
price_kg: number | null; price_kg: number | null;
weight: number | null; quantity: number | null;
quantity_unit: string | null;
type: string; type: string;
} }
@@ -43,7 +52,8 @@ export type ProductEdit = {
unit: string | null; unit: string | null;
price: number | null; price: number | null;
price_kg: number | null; price_kg: number | null;
weight: number | null; quantity: number | null;
quantity_unit: string | null;
type: string | null; type: string | null;
} }
@@ -53,7 +63,8 @@ export type ProductInputs = {
unit: string | null; unit: string | null;
price: number | null; price: number | null;
price_kg: number | null; price_kg: number | null;
weight: number | null; quantity: number | null;
quantity_unit: string | null;
type: string | null; type: string | null;
} }
@@ -69,7 +80,8 @@ export function productToProductInputs(product: Product): ProductInputs {
unit: product.unit, unit: product.unit,
price: product.price, price: product.price,
price_kg: product.price_kg, price_kg: product.price_kg,
weight: product.weight, quantity: product.quantity,
quantity_unit: product.quantity_unit,
type: product.type, type: product.type,
}; };
} }
@@ -81,7 +93,8 @@ export function productCreateFromProductInputs(productInput: ProductInputs): Pro
unit: productInput.unit!, unit: productInput.unit!,
price: productInput.price!, price: productInput.price!,
price_kg: productInput.price_kg, price_kg: productInput.price_kg,
weight: productInput.weight, quantity: productInput.quantity,
quantity_unit: productInput.quantity_unit,
type: productInput.type!, type: productInput.type!,
} }
} }