add i18n, products, productors and forms as tables

This commit is contained in:
2026-02-12 00:30:28 +01:00
parent 1813e2893e
commit 025b78d5dd
43 changed files with 1623 additions and 527 deletions

View File

@@ -16,7 +16,7 @@ async def get_forms(
return service.get_all(session, seasons, productors) return service.get_all(session, seasons, productors)
@router.get('/{id}', response_model=models.FormPublic) @router.get('/{id}', response_model=models.FormPublic)
async def get_forms(id: int, session: Session = Depends(get_session)): async def get_form(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id) result = service.get_one(session, id)
if result is None: if result is None:
raise HTTPException(status_code=404, detail=messages.notfound) raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -24,6 +24,7 @@ class ProductorBase(SQLModel):
name: str name: str
address: str address: str
payment: str payment: str
type: str
class ProductorPublic(ProductorBase): class ProductorPublic(ProductorBase):
id: int id: int
@@ -38,6 +39,7 @@ class ProductorUpdate(SQLModel):
name: str | None name: str | None
address: str | None address: str | None
payment: str | None payment: str | None
type: str | None
class ProductorCreate(ProductorBase): class ProductorCreate(ProductorBase):
pass pass

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,11 +8,15 @@ import src.productors.service as service
router = APIRouter(prefix='/productors') router = APIRouter(prefix='/productors')
@router.get('/', response_model=list[models.ProductorPublic]) @router.get('/', response_model=list[models.ProductorPublic])
def get_productors(session: Session = Depends(get_session)): def get_productors(
return service.get_all(session) names: list[str] = Query([]),
types: list[str] = Query([]),
session: Session = Depends(get_session)
):
return service.get_all(session, names, types)
@router.get('/{id}', response_model=models.ProductorPublic) @router.get('/{id}', response_model=models.ProductorPublic)
def get_productors(id: int, session: Session = Depends(get_session)): def get_productor(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id) result = service.get_one(session, id)
if result is None: if result is None:
raise HTTPException(status_code=404, detail=messages.notfound) raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -1,8 +1,12 @@
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.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:
statement.where(models.Productor.name.in_(names))
if len(types) > 0:
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

@@ -12,7 +12,7 @@ def get_templates(session: Session = Depends(get_session)):
return service.get_all(session) return service.get_all(session)
@router.get('/{id}', response_model=models.TemplatePublic) @router.get('/{id}', response_model=models.TemplatePublic)
def get_templates(id: int, session: Session = Depends(get_session)): def get_template(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id) result = service.get_one(session, id)
if result is None: if result is None:
raise HTTPException(status_code=404, detail=messages.notfound) raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -1,3 +1,69 @@
{ {
"product name": "product name",
"product price": "product price",
"product weight": "product weight",
"product type": "product type",
"planned": "planned",
"reccurent": "reccurent",
"product price kg": "product price kg",
"product unit": "product unit",
"grams": "grams",
"kilo": "kilo",
"piece": "piece",
"filter by season": "filter by season",
"name": "name",
"season": "season",
"start": "start",
"end": "end",
"productor": "productor",
"referer": "referer",
"edit form": "edit form",
"form name": "form name",
"contact season": "contact season",
"start date": "start date",
"end date": "end date",
"nothing found": "nothing found",
"number of shipment": "number of shipment",
"cancel": "cancel",
"create form": "create form",
"edit productor": "edit productor",
"remove productor": "remove productor",
"home": "home",
"dashboard": "dashboard",
"filter by name": "filter by name",
"filter by type": "filter by type",
"address": "address",
"payment": "payment",
"type": "type",
"create productor": "create productor",
"productor name": "productor name",
"productor type": "productor type",
"productor address": "productor address",
"productor payment": "productor payment",
"priceKg": "priceKg",
"weight": "weight",
"price": "price",
"create product": "create product",
"informations": "informations",
"remove product": "remove product",
"edit product": "edit product",
"shipment name": "shipment name",
"shipment date": "shipment date",
"remove shipment": "remove shipment",
"productors": "productors",
"products": "products",
"templates": "templates",
"users": "users",
"forms": "forms",
"all forms": "all forms",
"create new form": "create new form",
"actions": "actions",
"all productors": "all productors",
"all products": "all products",
"a name": "a name",
"a season": "a season",
"a start date": "a start date",
"a end date": "a end date",
"a productor": "a productor",
"a referer": "a referer"
} }

View File

@@ -1,3 +1,70 @@
{ {
"product name": "nom du produit",
"product price": "prix du produit",
"product weight": "poids du produit",
"product type": "type de produit",
"planned": "planifié",
"reccurent": "récurrent",
"product price kg": "prix du produit au Kilo",
"product unit": "unité de vente du produit",
"grams": "grammes",
"kilo": "kilo",
"piece": "pièce",
"filter by season": "filtrer par saisons",
"name": "nom",
"season": "saison",
"start": "début",
"end": "fin",
"productor": "producteur·trice",
"referer": "référent·e",
"edit form": "modifier le formulaire de contrat",
"form name": "nom du formulaire de contrat",
"contact season": "saison du contrat",
"start date": "date de début",
"end date": "date de fin",
"nothing found": "rien à afficher",
"number of shipment": "nombre de livraisons",
"cancel": "annuler",
"create form": "créer un formulare de contrat",
"edit productor": "modifier le producteur·trice",
"remove productor": "supprimer le producteur·trice",
"home": "accueil",
"dashboard": "tableau de bord",
"filter by name": "filtrer par nom",
"filter by type": "filtrer par type",
"address": "adresse",
"payment": "ordre du chèque",
"type": "type",
"create productor": "créer le producteur·troce",
"productor name": "nom du producteur·trice",
"productor type": "type du producteur·trice",
"productor address": "adresse du producteur·trice",
"productor payment": "ordre du chèque du producteur·trice",
"priceKg": "prix au kilo",
"weight": "poids",
"price": "prix",
"create product": "créer le produit",
"informations": "informations",
"remove product": "supprimer le produit",
"edit product": "modifier le produit",
"shipment name": "nom de la livraison",
"shipment date": "date de la livraison",
"remove shipment": "supprimer la livraison",
"productors": "producteur·trices",
"products": "produits",
"templates": "modèles",
"users": "utilisateur·trices",
"forms": "formulaires de contrat",
"all forms": "tous les formulaires de contrat",
"create new form": "créer un nouveau formulaire de contrat",
"actions": "actions",
"all productors": "tous les producteur·trices",
"all products": "tous les produits",
"is required": "est requis·e",
"a name": "un nom",
"a season": "une saison",
"a start date": "une date de début",
"a end date": "une date de fin",
"a productor": "un(e) producteur·trice",
"a referer": "un référent·e"
} }

View File

@@ -1,4 +1,4 @@
import { Grid, NumberInput, Paper, Select, Stack, TextInput } from "@mantine/core"; import { Grid, NumberInput, Select, Stack, TextInput } from "@mantine/core";
import { t } from "../../config/i18n"; import { t } from "../../config/i18n";
export type CreateProductProps = { export type CreateProductProps = {
@@ -11,22 +11,22 @@ export default function CreateProduct({form}: CreateProductProps) {
<Grid> <Grid>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}> <Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<TextInput <TextInput
label={t("product name")} label={t("product name", {capfirst: true})}
placeholder={t("product name")} placeholder={t("product name", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<NumberInput <NumberInput
label={t("product price")} label={t("product price", {capfirst: true})}
placeholder={t("product price")} placeholder={t("product price", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('price')} {...form.getInputProps('price')}
/> />
<TextInput <TextInput
label={t("product weight")} label={t("product weight", {capfirst: true})}
placeholder={t("product weight")} placeholder={t("product weight", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('weight')} {...form.getInputProps('weight')}
@@ -34,32 +34,32 @@ export default function CreateProduct({form}: CreateProductProps) {
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}> <Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<Select <Select
label={t("product type")} label={t("product type", {capfirst: true})}
placeholder={t("product type")} placeholder={t("product type", {capfirst: true})}
radius="sm" radius="sm"
data={[ data={[
{value: "1", label: t("planned")}, {value: "1", label: t("planned", {capfirst: true})},
{value: "2", label: t("reccurent")} {value: "2", label: t("reccurent", {capfirst: true})}
]} ]}
defaultValue={"1"} defaultValue={"1"}
clearable clearable
{...form.getInputProps('type')} {...form.getInputProps('type')}
/> />
<NumberInput <NumberInput
label={t("product price kg")} label={t("product price kg", {capfirst: true})}
placeholder={t("product price kg")} placeholder={t("product price kg", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('pricekg')} {...form.getInputProps('pricekg', {capfirst: true})}
/> />
<Select <Select
label={t("product unit")} label={t("product unit", {capfirst: true})}
placeholder={t("product unit")} placeholder={t("product unit", {capfirst: true})}
radius="sm" radius="sm"
data={[ data={[
{value: "1", label: t("grams")}, {value: "1", label: t("grams", {capfirst: true})},
{value: "2", label: t("kilo")}, {value: "2", label: t("kilo", {capfirst: true})},
{value: "3", label: t("piece")} {value: "3", label: t("piece", {capfirst: true})}
]} ]}
defaultValue={"2"} defaultValue={"2"}
clearable clearable

View File

@@ -1,62 +0,0 @@
import { Button, Group, Loader, Modal, Text, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { t } from "../../config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel, IconPlus } from "@tabler/icons-react";
import { createProductor, type Productor } from "../../services/api";
import { useEffect } from "react";
export function CreateProductorModal({opened, onClose}: ModalBaseProps) {
const form = useForm<Productor>();
const mutation = createProductor();
return (
<Modal
size="50%"
opened={opened}
onClose={onClose}
title={t("create productor")}
>
<Title order={4}>{t("Informations")}</Title>
<TextInput
label={t("productor name")}
placeholder={t("productor name")}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<TextInput
label={t("productor address")}
placeholder={t("productor address")}
radius="sm"
withAsterisk
{...form.getInputProps('address')}
/>
<TextInput
label={t("productor payment")}
placeholder={t("productor payment")}
radius="sm"
withAsterisk
{...form.getInputProps('payment')}
/>
{mutation.isError ? <Text>{t("an error occured")}:{mutation.error.message}</Text> : null}
{mutation.isSuccess ? <Text>{t("success")}</Text> : null}
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel")}
leftSection={<IconCancel/>}
onClick={onClose}
>{t("cancel")}</Button>
<Button
variant="filled"
aria-label={t("create productor")}
leftSection={mutation.isPending ? <Loader/> : <IconPlus/>}
onClick={() => {
mutation.mutate(form.getValues());
}}
>{t("create productor")}</Button>
</Group>
</Modal>
);
}

View File

@@ -1,5 +1,5 @@
import { Group, MultiSelect } from "@mantine/core"; import { Group, MultiSelect } from "@mantine/core";
import { t } from "../../../config/i18n"; import { t } from "@/config/i18n";
import { useMemo } from "react"; import { useMemo } from "react";
export type FilterFormsProps = { export type FilterFormsProps = {
@@ -9,7 +9,12 @@ export type FilterFormsProps = {
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} }
export function FilterForms({seasons, productors, filters, onFilterChange}: FilterFormsProps) { export default function FilterForms({
seasons,
productors,
filters,
onFilterChange
}: FilterFormsProps) {
const defaultProductors = useMemo(() => { const defaultProductors = useMemo(() => {
return filters.getAll("productors") return filters.getAll("productors")
}, [filters]); }, [filters]);
@@ -20,8 +25,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt
return ( return (
<Group> <Group>
<MultiSelect <MultiSelect
aria-label={t("filter by season")} aria-label={t("filter by season", {capfirst: true})}
placeholder={t("filter by season")} placeholder={t("filter by season", {capfirst: true})}
data={seasons} data={seasons}
defaultValue={defaultSeasons} defaultValue={defaultSeasons}
onChange={(values: string[]) => { onChange={(values: string[]) => {
@@ -30,8 +35,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt
clearable clearable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by productor")} aria-label={t("filter by productor", {capfirst: true})}
placeholder={t("filter by productor")} placeholder={t("filter by productor", {capfirst: true})}
data={productors} data={productors}
defaultValue={defaultProductors} defaultValue={defaultProductors}
onChange={(values: string[]) => { onChange={(values: string[]) => {

View File

@@ -1,77 +0,0 @@
import { ActionIcon, Badge, Box, Group, Paper, Text, Title } from "@mantine/core";
import { Link, useNavigate } from "react-router";
import { deleteForm, getForm, type Form, type Shipment } from "../../../services/api";
import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "../../../config/i18n";
import FormModal, { type FormInputs } from "../FormModal";
export type FormCardProps = {
form: Form;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (form: FormInputs, id?: number) => void;
}
export default function FormCard({form, isEdit, closeModal, handleSubmit}: FormCardProps) {
const deleteMutation = deleteForm();
const navigate = useNavigate();
const {data: currentForm, isPending} = getForm(form.id);
return (
<Paper
key={form.id}
shadow="xl"
p="xl"
miw={{base: "100vw", md: "25vw", lg:"20vw"}}
>
{/* TODO: Show only to logged users */}
<FormModal
opened={isEdit}
onClose={closeModal}
currentForm={currentForm}
handleSubmit={handleSubmit}
/>
<Group justify="space-between" mb="sm">
<ActionIcon
size={"sm"}
aria-label={t("edit form")}
onClick={(e) => {
e.stopPropagation();
navigate(`/form/${form.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
<ActionIcon
size={"sm"}
aria-label={t("delete form")}
color="red"
onClick={() => {
deleteMutation.mutate(form.id);
}}
>
<IconX/>
</ActionIcon>
</Group>
<Box
component={Link}
to={`/form/${form.id}`}
>
<Group justify="space-between" wrap="nowrap">
<Title
order={3}
textWrap="wrap"
lineClamp={1}
>
{form.name}
</Title>
<Badge>{form.season}</Badge>
</Group>
<Group justify="space-between">
<Text>{form.productor.name}</Text>
<Text>{form.referer.name}</Text>
</Group>
</Box>
</Paper>
);
}

View File

@@ -1,36 +1,26 @@
import { ActionIcon, Button, Collapse, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } from "@mantine/core"; import { ActionIcon, Button, Collapse, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } from "@mantine/core";
import { t } from "../../../config/i18n"; import { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { IconCancel, IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { IconCancel, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { getProductors, getUsers, type Form, type Shipment } from "../../../services/api"; import { getProductors, getUsers } from "@/services/api";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import ShipmentForm from "../../ShipmentForm"; import type { Form, FormInputs } from "@/services/resources/forms";
import type { ShipmentInputs } from "@/services/resources/shipments";
export type FormInputs = { import ShipmentForm from "@/components/Shipments/Form";
name: string;
season: string;
start: string | null;
end: string | null;
productor_id: string;
referer_id: string;
shipments: ShipmentInputs[];
}
export type ShipmentInputs = {
name: string | null;
date: string | null;
id: number | null;
form_id: number | null;
}
export type FormModalProps = ModalBaseProps & { export type FormModalProps = ModalBaseProps & {
currentForm?: Form; currentForm?: Form;
handleSubmit: (form: FormInputs, id?: number) => void; handleSubmit: (form: FormInputs, id?: number) => void;
} }
export default function FormModal({opened, onClose, currentForm, handleSubmit}: FormModalProps) { export default function FormModal({
opened,
onClose,
currentForm,
handleSubmit
}: FormModalProps) {
const {data: productors} = getProductors(); const {data: productors} = getProductors();
const {data: users} = getUsers(); const {data: users} = getUsers();
const form = useForm<FormInputs>({ const form = useForm<FormInputs>({
@@ -42,6 +32,20 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
productor_id: "", productor_id: "",
referer_id: "", referer_id: "",
shipments: [], shipments: [],
},
validate: {
name: (value) =>
!value ? `${t("a name", {capfirst: true})} ${t('is required')}` : null,
season: (value) =>
!value ? `${t("a season", {capfirst: true})} ${t('is required')}` : null,
start: (value) =>
!value ? `${t("a start date", {capfirst: true})} ${t('is required')}` : null,
end: (value) =>
!value ? `${t("a end date", {capfirst: true})} ${t('is required')}` : null,
productor_id: (value) =>
!value ? `${t("a productor", {capfirst: true})} ${t('is required')}` : null,
referer_id: (value) =>
!value ? `${t("a referer", {capfirst: true})} ${t('is required')}` : null
} }
}); });
@@ -68,7 +72,10 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
const [openedShipents, { toggle: toggleShipments }] = useDisclosure(true); const [openedShipents, { toggle: toggleShipments }] = useDisclosure(true);
const editShipmentElement = useCallback((index: number, shipment: ShipmentInputs) => { const editShipmentElement = useCallback((
index: number,
shipment: ShipmentInputs
) => {
form.setFieldValue('shipments', (prev) => { form.setFieldValue('shipments', (prev) => {
return prev.map((elem, id) => { return prev.map((elem, id) => {
if (id === index) if (id === index)
@@ -91,35 +98,35 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
title={currentForm ? t("edit form") : t('create form')} title={currentForm ? t("edit form") : t('create form')}
> >
<TextInput <TextInput
label={t("form name")} label={t("form name", {capfirst: true})}
placeholder={t("form name")} placeholder={t("form name", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <TextInput
label={t("contact season")} label={t("contact season", {capfirst: true})}
placeholder={t("contact season")} placeholder={t("contact season", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('season')} {...form.getInputProps('season')}
/> />
<DatePickerInput <DatePickerInput
label={t("start date")} label={t("start date", {capfirst: true})}
placeholder={t("start date")} placeholder={t("start date", {capfirst: true})}
withAsterisk withAsterisk
{...form.getInputProps('start')} {...form.getInputProps('start')}
/> />
<DatePickerInput <DatePickerInput
label={t("end date")} label={t("end date", {capfirst: true})}
placeholder={t("end date")} placeholder={t("end date", {capfirst: true})}
withAsterisk withAsterisk
{...form.getInputProps('end')} {...form.getInputProps('end')}
/> />
<Select <Select
label={t("referer")} label={t("referer", {capfirst: true})}
placeholder={t("referer")} placeholder={t("referer", {capfirst: true})}
nothingFoundMessage={t("nothing found")} nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk withAsterisk
clearable clearable
allowDeselect allowDeselect
@@ -128,9 +135,9 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
{...form.getInputProps('referer_id')} {...form.getInputProps('referer_id')}
/> />
<Select <Select
label={t("productor")} label={t("productor", {capfirst: true})}
placeholder={t("productor")} placeholder={t("productor", {capfirst: true})}
nothingFoundMessage={t("nothing found")} nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk withAsterisk
clearable clearable
allowDeselect allowDeselect
@@ -140,8 +147,8 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
/> />
<Group align="end"> <Group align="end">
<NumberInput <NumberInput
label={t("number of shipment")} label={t("number of shipment", {capfirst: true})}
placeholder={t("number of shipment")} placeholder={t("number of shipment", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
flex="2" flex="2"
@@ -162,7 +169,10 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
}) })
}} }}
/> />
<ActionIcon onClick={toggleShipments} disabled={form.getValues().shipments.length === 0}> <ActionIcon
onClick={toggleShipments}
disabled={form.getValues().shipments.length === 0}
>
{openedShipents ? <IconChevronUp/> : <IconChevronDown/>} {openedShipents ? <IconChevronUp/> : <IconChevronDown/>}
</ActionIcon> </ActionIcon>
</Group> </Group>
@@ -183,18 +193,23 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
<Button <Button
variant="filled" variant="filled"
color="red" color="red"
aria-label={t("cancel")} aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>} leftSection={<IconCancel/>}
onClick={() => { onClick={() => {
form.reset(); form.reset();
form.clearErrors();
onClose(); onClose();
}} }}
>{t("cancel")}</Button> >{t("cancel", {capfirst: true})}</Button>
<Button <Button
variant="filled" variant="filled"
aria-label={currentForm ? t("edit form") : t('create form')} aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
onClick={() => handleSubmit(form.getValues(), currentForm?.id)} onClick={() => {
>{currentForm ? t("edit form") : t('create form')}</Button> form.validate();
if (form.isValid())
handleSubmit(form.getValues(), currentForm?.id)
}}
>{currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}</Button>
</Group> </Group>
</Modal> </Modal>
); );

View File

@@ -0,0 +1,123 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate } from "react-router";
import { deleteForm, getForm} from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n";
import type { Form, FormInputs } from "@/services/resources/forms";
import FormModal from "@/components/Forms/Modal";
export type FormRowProps = {
form: Form;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (form: FormInputs, id?: number) => void;
}
export default function FormRow({
form,
isEdit,
closeModal,
handleSubmit
}: FormRowProps) {
const deleteMutation = deleteForm();
const navigate = useNavigate();
const {data: currentForm, isPending} = getForm(form.id);
return (
<Table.Tr key={form.id}>
<FormModal
opened={isEdit}
onClose={closeModal}
currentForm={currentForm}
handleSubmit={handleSubmit}
/>
<Table.Td>{form.name}</Table.Td>
<Table.Td>{form.season}</Table.Td>
<Table.Td>{form.start}</Table.Td>
<Table.Td>{form.end}</Table.Td>
<Table.Td>{form.productor.name}</Table.Td>
<Table.Td>{form.referer.name}</Table.Td>
<Table.Td>
<Tooltip label={t("edit productor", {capfirst: true})}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/forms/${form.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove productor", {capfirst: true})}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(form.id);
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
// <Paper
// key={form.id}
// shadow="xl"
// p="xl"
// miw={{base: "100vw", md: "25vw", lg:"20vw"}}
// >
// {/* TODO: Show only to logged users */}
// <FormModal
// opened={isEdit}
// onClose={closeModal}
// currentForm={currentForm}
// handleSubmit={handleSubmit}
// />
// <Group justify="space-between" mb="sm">
// <ActionIcon
// size={"sm"}
// aria-label={t("edit form", {capfirst: true})}
// onClick={(e) => {
// e.stopPropagation();
// navigate(`/dashboard/forms/${form.id}/edit`);
// }}
// >
// <IconEdit/>
// </ActionIcon>
// <ActionIcon
// size={"sm"}
// aria-label={t("delete form", {capfirst: true})}
// color="red"
// onClick={() => {
// deleteMutation.mutate(form.id);
// }}
// >
// <IconX/>
// </ActionIcon>
// </Group>
// <Box
// component={Link}
// to={`/form/${form.id}`}
// >
// <Group justify="space-between" wrap="nowrap">
// <Title
// order={3}
// textWrap="wrap"
// lineClamp={1}
// >
// {form.name}
// </Title>
// <Badge>{form.season}</Badge>
// </Group>
// <Group justify="space-between">
// <Text>{form.productor.name}</Text>
// <Text>{form.referer.name}</Text>
// </Group>
// </Box>
// </Paper>
);
}

View File

@@ -1,12 +1,12 @@
import { NavLink } from "react-router"; import { NavLink } from "react-router";
import { t } from "../../config/i18n"; import { t } from "@/config/i18n";
import "./index.css"; import "./index.css";
export function Navbar() { export function Navbar() {
return ( return (
<nav> <nav>
<NavLink to="/">{t("home")}</NavLink> <NavLink to="/">{t("home", {capfirst: true})}</NavLink>
<NavLink to="/dashboard">{t("dashboard")}</NavLink> <NavLink to="/dashboard">{t("dashboard", {capfirst: true})}</NavLink>
<NavLink to="/forms">{t("forms")}</NavLink>
</nav> </nav>
); );
} }

View File

@@ -0,0 +1,48 @@
import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react";
import { t } from "@/config/i18n";
export type ProductorsFiltersProps = {
names: string[];
types: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
export default function ProductorsFilter({
names,
types,
filters,
onFilterChange
}: ProductorsFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("name")
}, [filters]);
const defaultTypes = useMemo(() => {
return filters.getAll("type")
}, [filters]);
return (
<Group>
<MultiSelect
aria-label={t("filter by name", {capfirst: true})}
placeholder={t("filter by name", {capfirst: true})}
data={names}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, 'names')
}}
clearable
/>
<MultiSelect
aria-label={t("filter by type", {capfirst: true})}
placeholder={t("filter by type", {capfirst: true})}
data={types}
defaultValue={defaultTypes}
onChange={(values: string[]) => {
onFilterChange(values, 'types')
}}
clearable
/>
</Group>
);
}

View File

@@ -0,0 +1,108 @@
import { Button, Group, Modal, Text, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react";
import type { Productor, ProductorInputs } from "@/services/resources/productors";
import { useEffect } from "react";
export type ProductorModalProps = ModalBaseProps & {
currentProductor?: Productor;
handleSubmit: (productor: ProductorInputs, id?: number) => void;
}
export function ProductorModal({
opened,
onClose,
currentProductor,
handleSubmit
}: ProductorModalProps) {
const form = useForm<ProductorInputs>({
initialValues: {
name: "",
address: "",
payment: "",
type: "",
},
validate: {
name: (value) =>
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
address: (value) =>
!value ? `${t("address", {capfirst: true})} ${t('is required')}` : null,
payment: (value) =>
!value ? `${t("payment", {capfirst: true})} ${t('is required')}` : null,
type: (value) =>
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null
}
});
useEffect(() => {
if (currentProductor) {
form.initialize({
...currentProductor,
});
}
}, [currentProductor]);
return (
<Modal
size="50%"
opened={opened}
onClose={onClose}
title={t("create productor", {capfirst: true})}
>
<Title order={4}>{t("Informations", {capfirst: true})}</Title>
<TextInput
label={t("productor name", {capfirst: true})}
placeholder={t("productor name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<TextInput
label={t("productor type", {capfirst: true})}
placeholder={t("productor type", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('type')}
/>
<TextInput
label={t("productor address", {capfirst: true})}
placeholder={t("productor address", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('address')}
/>
<TextInput
label={t("productor payment", {capfirst: true})}
placeholder={t("productor payment", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('payment')}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>}
onClick={() => {
form.reset();
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
<Button
variant="filled"
aria-label={currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentProductor?.id)
}
}}
>{currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}</Button>
</Group>
</Modal>
);
}

View File

@@ -0,0 +1,67 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react";
import type { Productor, ProductorInputs } from "@/services/resources/productors";
import { ProductorModal } from "@/components/Productors/Modal";
import { deleteProductor, getProductor } from "@/services/api";
import { useNavigate } from "react-router";
export type ProductorRowProps = {
productor: Productor;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (productor: ProductorInputs, id?: number) => void;
}
export default function ProductorRow({
productor,
isEdit,
closeModal,
handleSubmit
}: ProductorRowProps) {
const deleteMutation = deleteProductor();
const navigate = useNavigate();
const {data: currentProductor, isPending} = getProductor(productor.id);
return (
<Table.Tr key={productor.id}>
<ProductorModal
opened={isEdit}
onClose={closeModal}
currentProductor={currentProductor}
handleSubmit={handleSubmit}
/>
<Table.Td>{productor.name}</Table.Td>
<Table.Td>{productor.type}</Table.Td>
<Table.Td>{productor.address}</Table.Td>
<Table.Td>{productor.payment}</Table.Td>
<Table.Td>
<Tooltip label={t("edit productor", {capfirst: true})}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/productors/${productor.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove productor", {capfirst: true})}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(productor.id);
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

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

View File

@@ -0,0 +1,139 @@
import { Button, Group, Modal, NumberInput, Select, TextInput, Title, type ModalBaseProps } from "@mantine/core";
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 { useEffect, useMemo } from "react";
import { getProductors } from "@/services/api";
export type ProductModalProps = ModalBaseProps & {
currentProduct?: Product;
handleSubmit: (product: ProductInputs, id?: number) => void;
}
export function ProductModal({
opened,
onClose,
currentProduct,
handleSubmit
}: ProductModalProps) {
const {data: productors} = getProductors();
const form = useForm<ProductInputs>({
initialValues: {
name: "",
unit: null,
price: null,
priceKg: null,
weight: null,
type: "",
productor_id: null,
},
validate: {
name: (value) =>
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
unit: (value) =>
!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) =>
!value ? `${t("productor", {capfirst: true})} ${t('is required')}` : null
}
});
useEffect(() => {
if (currentProduct) {
form.initialize({
...currentProduct,
});
}
}, [currentProduct]);
const productorsSelect = useMemo(() => {
return productors?.map(prod => ({value: String(prod.id), label: `${prod.name}`}))
}, [productors])
return (
<Modal
size="50%"
opened={opened}
onClose={onClose}
title={t("create product", {capfirst: true})}
>
<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')}
/>
<TextInput
label={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})}
placeholder={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('type')}
/>
<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}, {capfirst: true})}
placeholder={t("product priceKg", {capfirst: true}, {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('priceKg')}
/>
<NumberInput
label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('weight', {capfirst: true})}
/>
<Select
label={t("productor", {capfirst: true})}
placeholder={t("productor")}
nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk
clearable
allowDeselect
searchable
data={productorsSelect || []}
{...form.getInputProps('productor_id')}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>}
onClick={() => {
form.reset();
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
<Button
variant="filled"
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
onClick={() => {
form.validate();
if (form.isValid())
handleSubmit(form.getValues(), currentProduct?.id)
}}
>{currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}</Button>
</Group>
</Modal>
);
}

View File

@@ -0,0 +1,69 @@
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 { ProductModal } from "@/components/Products/Modal";
import { deleteProduct, getProduct } from "@/services/api";
import { useNavigate } from "react-router";
export type ProductRowProps = {
product: Product;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (product: ProductInputs, id?: number) => void;
}
export default function ProductRow({
product,
isEdit,
closeModal,
handleSubmit
}: ProductRowProps) {
const deleteMutation = deleteProduct();
const navigate = useNavigate();
const {data: currentProduct, isPending} = getProduct(product.id);
return (
<Table.Tr key={product.id}>
<ProductModal
opened={isEdit}
onClose={closeModal}
currentProduct={currentProduct}
handleSubmit={handleSubmit}
/>
<Table.Td>{product.name}</Table.Td>
<Table.Td>{product.type}</Table.Td>
<Table.Td>{product.price}</Table.Td>
<Table.Td>{product.priceKg}</Table.Td>
<Table.Td>{product.weight}</Table.Td>
<Table.Td>{product.unit}</Table.Td>
<Table.Td>
<Tooltip label={t("edit product", {capfirst: true})}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/products/${product.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove product", {capfirst: true})}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(product.id);
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -1,50 +0,0 @@
import { Group, NumberInput, Paper, Stack, Text } from "@mantine/core";
import { useCallback, useState } from "react";
type Product = {
name: string;
price: number;
priceKg: number;
unit: string;
}
export type ShipmentCardProps = {
title: string;
date: string;
product: Product;
}
export default function ShipmentCard({title, date, product}: ShipmentCardProps) {
const [ price, setPrice ] = useState<number>(0)
const calculatePrice = useCallback((value: number | string ) => {
const numberValue = Number(value)
const price = numberValue * product.price
setPrice(price)
}, [])
return (
<Paper shadow="xs" p="xl">
<Group justify="space-between">
<Text>{title}</Text>
<Text>{date}</Text>
</Group>
<Text>{product.name}</Text>
<Group>
<Stack align="flex-start">
<Text>{product.price} / piece</Text>
<Text>{product.priceKg} / kilo</Text>
</Stack>
<NumberInput
aria-label="select quantity"
description={`Indiquez le nombre de ${product.unit}`}
placeholder={`Quantité en ${product.unit}`}
allowNegative={false}
onChange={calculatePrice}
/>
<Text size="xs">
{new Intl.NumberFormat('en-us', {minimumFractionDigits: 2}).format(price)}
</Text>
</Group>
</Paper>
);
}

View File

@@ -1,9 +1,8 @@
import { ActionIcon, Group, TextInput, Tooltip } from "@mantine/core"; import { ActionIcon, Group, TextInput, Tooltip } from "@mantine/core";
import { DatePickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { t } from "../../config/i18n";
import type { Shipment } from "../../services/api";
import { IconX } from "@tabler/icons-react"; import { IconX } from "@tabler/icons-react";
import type { ShipmentInputs } from "../Forms/FormModal"; import { t } from "@/config/i18n";
import type { ShipmentInputs } from "@/services/resources/shipments";
export type ShipmentFormProps = { export type ShipmentFormProps = {
index: number; index: number;
@@ -22,8 +21,8 @@ export default function ShipmentForm({
<Group justify="space-between" key={`shipment_${index}`}> <Group justify="space-between" key={`shipment_${index}`}>
<Group grow maw="80%"> <Group grow maw="80%">
<TextInput <TextInput
label={t("shipment name")} label={t("shipment name", {capfirst: true})}
placeholder={t("shipment name")} placeholder={t("shipment name", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
value={shipment.name || ""} value={shipment.name || ""}
@@ -33,23 +32,23 @@ export default function ShipmentForm({
}} }}
/> />
<DatePickerInput <DatePickerInput
label={t("shipment date")} label={t("shipment date", {capfirst: true})}
placeholder={t("shipment date")} placeholder={t("shipment date", {capfirst: true})}
radius="sm" radius="sm"
withAsterisk withAsterisk
value={shipment.date} value={shipment.date || null}
onChange={(event) => { onChange={(event) => {
const value = event || ""; const value = event || "";
setShipmentElement(index, {...shipment, date: value}) setShipmentElement(index, {...shipment, date: value})
}} }}
/> />
</Group> </Group>
<Tooltip label={t("remove shipment")}> <Tooltip label={t("remove shipment", {capfirst: true})}>
<ActionIcon <ActionIcon
flex={{base: "1", md: "0"}} flex={{base: "1", md: "0"}}
style={{alignSelf: "flex-end"}} style={{alignSelf: "flex-end"}}
color="red" color="red"
aria-label={t("remove shipment")} aria-label={t("remove shipment", {capfirst: true})}
onClick={() => { onClick={() => {
deleteShipmentElement(index) deleteShipmentElement(index)
}} }}

View File

@@ -5,9 +5,9 @@ import LanguageDetector from "i18next-browser-languagedetector";
import { Settings } from "luxon"; import { Settings } from "luxon";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import en from "../../locales/en.json"; import en from "@/../locales/en.json";
import fr from "../../locales/fr.json"; import fr from "@/../locales/fr.json";
import { Config } from "./config"; import { Config } from "@/config/config";
const resources = { const resources = {
en: { translation: en }, en: { translation: en },
@@ -32,10 +32,25 @@ i18next
}) })
.then(() => { .then(() => {
[Settings.defaultLocale] = i18next.language.split("-"); [Settings.defaultLocale] = i18next.language.split("-");
i18next.services.formatter?.add(
"capfirst",
(value) => {
if (typeof value !== "string" || !value.length) {
return value;
}
return value.charAt(0).toUpperCase() + value.slice(1);
}
);
}); });
export function t(message: string, params?: Record<string, any>) {
return i18next.t(message, params); export function t(message: string, params?: Record<string, any> & {capfirst?: boolean}) {
const result = i18next.t(message, params);
if (params?.capfirst && typeof result === "string" && result.length) {
return result.charAt(0).toUpperCase() + result.slice(1);
}
return result;
} }
export default i18next; export default i18next;

View File

@@ -1,7 +1,7 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router"; import { RouterProvider } from "react-router";
import { router } from "./router.tsx"; import { router } from "@/router.tsx";
import { MantineProvider } from "@mantine/core"; import { MantineProvider } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';

View File

@@ -0,0 +1,28 @@
import { Tabs } from "@mantine/core";
import { t } from "@/config/i18n";
import { Outlet, useNavigate, useParams } from "react-router";
export default function Dashboard() {
const navigate = useNavigate();
const { tabValue } = useParams();
return (
<Tabs
w={{base: "100%", md: "80%", lg: "60%"}}
keepMounted={false}
defaultValue="productors"
orientation={"horizontal"}
value={tabValue}
onChange={(value) => navigate(`/dashboard/${value}`)}
>
<Tabs.List>
<Tabs.Tab value="productors">{t("productors", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="products">{t("products", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="templates">{t("templates", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="users">{t("users", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="forms">{t("forms", {capfirst: true})}</Tabs.Tab>
</Tabs.List>
<Outlet/>
</Tabs>
);
}

View File

@@ -1,79 +0,0 @@
import { Flex, Grid, Select, Stack, Text, TextInput, Title } from "@mantine/core";
import { IconUser } from "@tabler/icons-react";
import { getForm } from "../../../services/api";
import { t } from "../../../config/i18n";
import ShipmentCard from "../../../components/ShipmentCard";
export function ReadForm() {
// const { isPending, error, data } = getForm(1);
// console.log(isPending, error, data);
return (
<Flex
w={{base: "100%", sm: "50%", lg: "60%"}}
justify={"start"}
align={"flex-start"}
direction={"column"}
>
{/* <Stack>
<Title>{t("form contract")}</Title>
<Text>{t("contract description that is rather long to show how text will be displayed even with unnecessary elements like this end of sentence")}</Text>
</Stack>
<Stack>
<Text>{t("contact phase")}</Text>
<Grid>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<TextInput
radius="sm"
label={t("firstname")}
placeholder={t("firstname")}
leftSection={<IconUser/>}
/>
<TextInput
radius="sm"
label={t("lastname")}
placeholder={t("lastname")}
leftSection={<IconUser/>}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<TextInput
radius="sm"
label={t("email")}
placeholder={t("email")}
leftSection={<IconUser/>}
/>
<TextInput
radius="sm"
label={t("phone")}
placeholder={t("phone")}
leftSection={<IconUser/>}
/>
</Grid.Col>
</Grid>
</Stack>
<Stack>
<Text>{t("products reccurent phase")}</Text>
{isPending ||
<Select
label={`${t("select reccurent product")} (${t("this product will be distributed for all shipments")})`}
placeholder={t("select reccurent product")}
data={data?.productor?.products.map(el=> el.name)}
/>
}
</Stack>
<Stack>
<Text>{t("products planned phase")}</Text>
<ShipmentCard
title="Shipment 1"
date="2025-10-10"
product={{
name: "rognons de veau",
price: 1.20,
priceKg: 10.9,
unit: "piece"
}}
/>
</Stack> */}
</Flex>
);
}

View File

@@ -1,23 +1,25 @@
import { Stack, Loader, Title, Group, ActionIcon, Flex, Tooltip } from "@mantine/core"; import { Stack, Loader, Title, Group, ActionIcon, Tooltip, Table, ScrollArea } from "@mantine/core";
import { createForm, createShipment, editForm, editShipment, getForms, type Form } from "../../services/api"; import { createForm, createShipment, editForm, editShipment, getForms } from "@/services/api";
import { t } from "../../config/i18n"; import { t } from "@/config/i18n";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { IconPlus } from "@tabler/icons-react"; import { IconPlus } from "@tabler/icons-react";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { FilterForms } from "../../components/Forms/FilterForms"; import FormModal from "@/components/Forms/Modal";
import FormCard from "../../components/Forms/FormCard"; import FormRow from "@/components/Forms/Row";
import FormModal, { type FormInputs, type ShipmentInputs } from "../../components/Forms/FormModal"; import type { Form, FormInputs } from "@/services/resources/forms";
import type { ShipmentEdit, ShipmentInputs } from "@/services/resources/shipments";
import FilterForms from "@/components/Forms/Filter";
export function Forms() { export function Forms() {
const [ searchParams, setSearchParams ] = useSearchParams(); const [ searchParams, setSearchParams ] = useSearchParams();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const isCreate = location.pathname === "/form/create"; const isCreate = location.pathname === "/dashboard/forms/create";
const isEdit = location.pathname.includes("/edit"); const isEdit = location.pathname.includes("/edit");
const closeModal = () => { const closeModal = () => {
navigate("/forms"); navigate("/dashboard/forms");
}; };
const { isPending, data } = getForms(searchParams); const { isPending, data } = getForms(searchParams);
@@ -43,7 +45,6 @@ export function Forms() {
return; return;
const newForm = await createFormMutation.mutateAsync({ const newForm = await createFormMutation.mutateAsync({
...form, ...form,
shipment_ids: [],
start: form?.start, start: form?.start,
end: form?.start, end: form?.start,
productor_id: Number(form.productor_id), productor_id: Number(form.productor_id),
@@ -61,50 +62,62 @@ export function Forms() {
); );
}); });
closeModal(); closeModal();
}, []); }, [createFormMutation, createShipmentsMutation]);
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => { const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
if (!id) if (!id)
return; return;
// edit all existing shipments form.shipments
// edit form .filter((el: ShipmentInputs) => el.id)
form.shipments.filter(el => el.id).map(async (shipment) => { .map(async (shipment: ShipmentInputs) => {
if (!shipment.name || !shipment.date || !shipment.form_id || !shipment.id) if (
!shipment.name ||
!shipment.date ||
!shipment.form_id ||
!shipment.id
)
return return
const newShipment = { const newShipment: ShipmentEdit = {
name: shipment.name, name: shipment.name,
date: shipment.date, date: shipment.date,
form_id: shipment.form_id, form_id: shipment.form_id,
} };
await editShipmentsMutation.mutate({id: shipment.id, shipment: newShipment}) await editShipmentsMutation.mutate({
id: shipment.id,
shipment: newShipment
});
}); });
const newForm = await editFormMutation.mutateAsync({ const newForm = await editFormMutation.mutateAsync({
id: id, id: id,
form: { form: {
...form, ...form,
shipment_ids: [],
start: form.start, start: form.start,
end: form.start, end: form.start,
productor_id: Number(form.productor_id), productor_id: Number(form.productor_id),
referer_id: Number(form.referer_id) referer_id: Number(form.referer_id)
} }
}); });
// if shipments to add -> create shipments form.shipments
form.shipments.filter(el => el.id === null).map(async (shipment) => { .filter((el: ShipmentInputs) => el.id === null)
.map(async (shipment: ShipmentInputs) => {
if (!shipment.name || !shipment.date) if (!shipment.name || !shipment.date)
return return
const newShipment = { const newShipment = {
name: shipment.name, name: shipment.name,
date: shipment.date, date: shipment.date,
} }
return await createShipmentsMutation.mutateAsync( return await createShipmentsMutation.mutateAsync({
{...newShipment, form_id: newForm.id} ...newShipment,
); form_id: newForm.id,
});
}); });
closeModal(); closeModal();
}, []); }, [editShipmentsMutation, createShipmentsMutation, editFormMutation]);
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)
@@ -121,21 +134,24 @@ export function Forms() {
return (<Loader color="blue"/>); return (<Loader color="blue"/>);
return ( return (
<Stack w={{base: "100%", sm: "60%", lg: "60%"}}> <Stack>
<Group justify="space-between"> <Group justify="space-between">
<Title order={1}>{t("All forms")}</Title> <Title order={2}>{t("all forms", {capfirst: true})}</Title>
<Tooltip label={t("create new form")}> <Tooltip label={t("create new form", {capfirst: true})}>
<ActionIcon <ActionIcon
size="xl"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/form/create`); navigate(`/dashboard/forms/create`);
}} }}
> >
<IconPlus/> <IconPlus/>
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<FormModal opened={isCreate} onClose={closeModal} handleSubmit={handleCreateForm}/> <FormModal
opened={isCreate}
onClose={closeModal}
handleSubmit={handleCreateForm}
/>
</Group> </Group>
<FilterForms <FilterForms
productors={productors || []} productors={productors || []}
@@ -143,13 +159,41 @@ export function Forms() {
filters={searchParams} filters={searchParams}
onFilterChange={onFilterChange} onFilterChange={onFilterChange}
/> />
<Flex gap="md" wrap="wrap" justify="center"> <ScrollArea type="auto">
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("name", {capfirst: true})}</Table.Th>
<Table.Th>{t("type", {capfirst: true})}</Table.Th>
<Table.Th>{t("start", {capfirst: true})}</Table.Th>
<Table.Th>{t("end", {capfirst: true})}</Table.Th>
<Table.Th>{t("productor", {capfirst: true})}</Table.Th>
<Table.Th>{t("referer", {capfirst: true})}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
data.map((form) => (
<FormRow
form={form}
isEdit={isEdit}
closeModal={closeModal}
handleSubmit={handleEditForm}
/>
))
}
</Table.Tbody>
</Table>
</ScrollArea>
{/* <Flex gap="md" wrap="wrap" justify="center">
{ {
data?.map((form: Form) => ( data?.map((form: Form) => (
<FormCard form={form} isEdit={isEdit} closeModal={closeModal} handleSubmit={handleEditForm}/> <FormCard form={form} isEdit={isEdit} closeModal={closeModal} handleSubmit={handleEditForm}/>
)) ))
} }
</Flex> </Flex> */}
</Stack> </Stack>
); );
} }

View File

@@ -1,8 +1,8 @@
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { t } from "../../config/i18n"; import { t } from "@/config/i18n";
export function Home() { export function Home() {
return ( return (
<Text>{t("test")}</Text> <Text>{t("test", {capfirst: true})}</Text>
); );
} }

View File

@@ -0,0 +1,127 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { createProductor, editProductor, getProductors } from "@/services/api";
import { IconPlus } from "@tabler/icons-react";
import ProductorRow from "@/components/Productors/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { ProductorModal } from "@/components/Productors/Modal";
import { useCallback, useMemo } from "react";
import type { Productor, ProductorInputs } from "@/services/resources/productors";
import ProductorsFilters from "@/components/Productors/Filter";
export default function Productors() {
const [ searchParams, setSearchParams ] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const isCreate = location.pathname === "/dashboard/productors/create";
const isEdit = location.pathname.includes("/edit");
const closeModal = () => {
navigate("/dashboard/productors");
};
const {data: productors, isPending} = getProductors(searchParams);
const {data: allProductors } = getProductors();
const names = useMemo(() => {
return allProductors?.map((productor: Productor) => (productor.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allProductors])
const types = useMemo(() => {
return allProductors?.map((productor: Productor) => (productor.type))
.filter((productor, index, array) => array.indexOf(productor) === index)
}, [allProductors])
const createProductorMutation = createProductor();
const editProductorMutation = editProductor();
const handleCreateProductor = useCallback(async (productor: ProductorInputs) => {
await createProductorMutation.mutateAsync({
...productor
});
closeModal();
}, [createProductorMutation]);
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => {
if (!id)
return;
await editProductorMutation.mutateAsync({
id: id,
productor: productor
});
closeModal();
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
params.delete(filter)
values.forEach(value => {
params.append(filter, value);
});
return params;
});
}, [searchParams, setSearchParams])
if (!productors || isPending)
return <Loader/>
return (
<Stack>
<Group justify="space-between">
<Title order={2}>{t("all productors", {capfirst: true})}</Title>
<Tooltip label={t("create productor", {capfirst: true})}>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/productors/create`);
}}
>
<IconPlus/>
</ActionIcon>
</Tooltip>
<ProductorModal
opened={isCreate}
onClose={closeModal}
handleSubmit={handleCreateProductor}
/>
</Group>
<ProductorsFilters
names={names || []}
types={types || []}
filters={searchParams}
onFilterChange={onFilterChange}
/>
<ScrollArea type="auto">
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("name", {capfirst: true})}</Table.Th>
<Table.Th>{t("type", {capfirst: true})}</Table.Th>
<Table.Th>{t("address", {capfirst: true})}</Table.Th>
<Table.Th>{t("payment", {capfirst: true})}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
productors.map((productor) => (
<ProductorRow
productor={productor}
isEdit={isEdit}
closeModal={closeModal}
handleSubmit={handleEditProductor}
/>
))
}
</Table.Tbody>
</Table>
</ScrollArea>
</Stack>
);
}

View File

@@ -0,0 +1,129 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { createProduct, editProduct, getProducts } from "@/services/api";
import { IconPlus } from "@tabler/icons-react";
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 ProductsFilters from "@/components/Products/Filter";
export default function Products() {
const [ searchParams, setSearchParams ] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const isCreate = location.pathname === "/dashboard/products/create";
const isEdit = location.pathname.includes("/edit");
const closeModal = () => {
navigate("/dashboard/products");
};
const {data: products, isPending} = getProducts(searchParams);
const {data: allProducts } = getProducts();
const names = useMemo(() => {
return allProducts?.map((product: Product) => (product.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allProducts])
const types = useMemo(() => {
return allProducts?.map((product: Product) => (product.type))
.filter((product, index, array) => array.indexOf(product) === index)
}, [allProducts])
const createProductMutation = createProduct();
const editProductMutation = editProduct();
const handleCreateProduct = useCallback(async (product: ProductInputs) => {
await createProductMutation.mutateAsync({
...product
});
closeModal();
}, [createProductMutation]);
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => {
if (!id)
return;
await editProductMutation.mutateAsync({
id: id,
product: product
});
closeModal();
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
params.delete(filter)
values.forEach(value => {
params.append(filter, value);
});
return params;
});
}, [searchParams, setSearchParams])
if (!products || isPending)
return <Loader/>
return (
<Stack>
<Group justify="space-between">
<Title order={2}>{t("all products", {capfirst: true})}</Title>
<Tooltip label={t("create product", {capfirst: true})}>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/products/create`);
}}
>
<IconPlus/>
</ActionIcon>
</Tooltip>
<ProductModal
opened={isCreate}
onClose={closeModal}
handleSubmit={handleCreateProduct}
/>
</Group>
<ProductsFilters
names={names || []}
types={types || []}
filters={searchParams}
onFilterChange={onFilterChange}
/>
<ScrollArea type="auto">
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("name", {capfirst: true})}</Table.Th>
<Table.Th>{t("type", {capfirst: true})}</Table.Th>
<Table.Th>{t("price", {capfirst: true})}</Table.Th>
<Table.Th>{t("priceKg", {capfirst: true})}</Table.Th>
<Table.Th>{t("weight", {capfirst: true})}</Table.Th>
<Table.Th>{t("unit", {capfirst: true})}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
products.map((product) => (
<ProductRow
product={product}
isEdit={isEdit}
closeModal={closeModal}
handleSubmit={handleEditProduct}
/>
))
}
</Table.Tbody>
</Table>
</ScrollArea>
</Stack>
);
}

View File

@@ -0,0 +1,5 @@
export default function Templates() {
return (
<></>
);
}

View File

@@ -0,0 +1,5 @@
export default function Users() {
return (
<></>
);
}

View File

@@ -1,6 +1,6 @@
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import { Navbar } from "./components/Navbar"; import { Navbar } from "@/components/Navbar";
import { Footer } from "./components/Footer"; import { Footer } from "@/components/Footer";
export default function Root() { export default function Root() {
return ( return (

View File

@@ -2,11 +2,15 @@ import {
createBrowserRouter, createBrowserRouter,
} from "react-router"; } from "react-router";
import Root from "./root"; import Root from "@/root";
import { Home } from "./pages/Home"; import { Home } from "@/pages/Home";
import { Forms } from "./pages/Forms"; import { Forms } from "@/pages/Forms";
import { ReadForm } from "./pages/Forms/ReadForm" import Dashboard from "@/pages/Dashboard";
// import { CreateForms } from "./pages/Forms/CreateForm"; import Productors from "@/pages/Productors";
import Products from "@/pages/Products";
import Templates from "@/pages/Templates";
import Users from "@/pages/Users";
// import { CreateForms } from "@/pages/Forms/CreateForm";
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {
@@ -16,9 +20,21 @@ export const router = createBrowserRouter([
children: [ children: [
{ index: true, Component: Home }, { index: true, Component: Home },
{ path: "/forms", Component: Forms }, { path: "/forms", Component: Forms },
{ path: "/form/:id", Component: ReadForm }, { path: "/dashboard", Component: Dashboard, children: [
{ path: "/form/:id/edit", Component: Forms }, {path: "productors", Component: Productors},
{ path: "/form/create", Component: Forms }, {path: "productors/create", Component: Productors},
{path: "productors/:id/edit", Component: Productors},
{ path: "products", Component: Products },
{path: "products/create", Component: Products},
{path: "products/:id/edit", Component: Products},
{ path: "templates", Component: Templates },
{ path: "users", Component: Users },
{ path: "forms", Component: Forms },
{ path: "forms/:id/edit", Component: Forms },
{ path: "forms/create", Component: Forms },
] },
// { path: "/form/:id", Component: ReadForm },
], ],
}, },
]); ]);

View File

@@ -1,85 +1,16 @@
import { useMutation, useQuery, useQueryClient,type UseQueryResult } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient,type UseQueryResult } from "@tanstack/react-query";
import { Config } from "../config/config"; import { Config } from "@/config/config";
import type { Form, FormCreate, FormEditPayload } from "@/services/resources/forms";
import type { Shipment, ShipmentCreate, ShipmentEditPayload } from "@/services/resources/shipments";
import type { Productor, ProductorCreate, ProductorEditPayload } from "@/services/resources/productors";
import type { User } from "@/services/resources/users";
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
export type Productor = { export function getUsers() {
id: number; return useQuery<User[]>({
name: string; queryKey: ['users'],
address: string;
payment: string;
}
export type Shipment = {
name: string;
date: string;
id?: number;
form_id: number;
}
export type Form = {
id: number;
name: string;
season: string;
start: string;
end: string;
productor: Productor;
referer: User;
shipments: Shipment[];
}
export type FormCreate = {
name: string;
season: string;
start: string;
end: string;
productor_id: number;
referer_id: number;
shipment_ids: Shipment[];
}
export type FormEdit = {
name?: string | null;
season?: string | null;
start?: string | null;
end?: string | null;
productor_id?: number | null;
referer_id?: number | null;
shipment_ids: Shipment[];
}
export type Product = {
id: number;
productor: Productor;
name: string;
unit: number;
price: number;
priceKg: number | null;
weight: number;
type: number;
}
export type User = {
id: number;
name: string;
email: string;
products: Product[];
}
export function getForm(id: number): UseQueryResult<Form, Error> {
return useQuery<Form>({
queryKey: ['form'],
queryFn: () => ( queryFn: () => (
fetch(`${Config.backend_uri}/forms/${id}`) fetch(`${Config.backend_uri}/users`)
.then((res) => res.json())
),
});
}
export function getForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> {
const queryString = filters?.toString()
return useQuery<Form[]>({
queryKey: ['forms', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`)
.then((res) => res.json()) .then((res) => res.json())
), ),
}); });
@@ -95,32 +26,11 @@ export function getShipments() {
}); });
} }
export function getProductors() {
return useQuery<Productor[]>({
queryKey: ['productors'],
queryFn: () => (
fetch(`${Config.backend_uri}/productors`)
.then((res) => res.json())
),
});
}
export function getUsers() {
return useQuery<User[]>({
queryKey: ['users'],
queryFn: () => (
fetch(`${Config.backend_uri}/users`)
.then((res) => res.json())
),
});
}
export function createShipment() { export function createShipment() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (newShipment: Shipment) => { mutationFn: (newShipment: ShipmentCreate) => {
return fetch(`${Config.backend_uri}/shipments`, { return fetch(`${Config.backend_uri}/shipments`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -136,11 +46,6 @@ export function createShipment() {
}) })
} }
export type ShipmentEditPayload = {
id: number;
shipment: Shipment;
}
export function editShipment() { export function editShipment() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -160,11 +65,32 @@ export function editShipment() {
}) })
} }
export function getProductors(filters?: URLSearchParams) {
const queryString = filters?.toString()
return useQuery<Productor[]>({
queryKey: ['productors', filters],
queryFn: () => (
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
),
});
}
export function getProductor(id: number) {
return useQuery<Productor>({
queryKey: ['productor'],
queryFn: () => (
fetch(`${Config.backend_uri}/productors/${id}`)
.then((res) => res.json())
),
});
}
export function createProductor() { export function createProductor() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (newProductor: Productor) => { mutationFn: (newProductor: ProductorCreate) => {
return fetch(`${Config.backend_uri}/productors`, { return fetch(`${Config.backend_uri}/productors`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -179,6 +105,63 @@ export function createProductor() {
}) })
} }
export function editProductor() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({productor, id}: ProductorEditPayload) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
method: 'PUT',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(productor),
}).then((res) => res.json());
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['productors'] })
}
})
}
export function deleteProductor() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
method: 'DELETE',
headers: {
"Content-Type": "application/json"
},
}).then((res) => res.json());
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['productors'] })
}
});
}
export function getForm(id: number): UseQueryResult<Form, Error> {
return useQuery<Form>({
queryKey: ['form'],
queryFn: () => (
fetch(`${Config.backend_uri}/forms/${id}`)
.then((res) => res.json())
),
});
}
export function getForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> {
const queryString = filters?.toString()
return useQuery<Form[]>({
queryKey: ['forms', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
),
});
}
export function createForm() { export function createForm() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -215,11 +198,6 @@ export function deleteForm() {
}); });
} }
export type FormEditPayload = {
id: number;
form: FormEdit;
}
export function editForm() { export function editForm() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -238,3 +216,79 @@ export function editForm() {
} }
}); });
} }
export function getProduct(id: number): UseQueryResult<Product, Error> {
return useQuery<Product>({
queryKey: ['product'],
queryFn: () => (
fetch(`${Config.backend_uri}/products/${id}`)
.then((res) => res.json())
),
});
}
export function getProducts(filters?: URLSearchParams): UseQueryResult<Product[], Error> {
const queryString = filters?.toString()
return useQuery<Product[]>({
queryKey: ['products', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
),
});
}
export function createProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (newProduct: ProductCreate) => {
return fetch(`${Config.backend_uri}/products`, {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(newProduct),
}).then((res) => res.json());
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['products'] })
}
});
}
export function deleteProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
method: 'DELETE',
headers: {
"Content-Type": "application/json"
},
}).then((res) => res.json());
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['products'] })
}
});
}
export function editProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({id, product}: ProductEditPayload) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
method: 'PUT',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(product),
}).then((res) => res.json());
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['products'] })
}
});
}

View File

@@ -0,0 +1,47 @@
import type { Productor } from "@/services/resources/productors";
import type { Shipment, ShipmentInputs } from "@/services/resources/shipments";
import type { User } from "@/services/resources/users";
export type Form = {
id: number;
name: string;
season: string;
start: string;
end: string;
productor: Productor;
referer: User;
shipments: Shipment[];
}
export type FormCreate = {
name: string;
season: string;
start: string;
end: string;
productor_id: number;
referer_id: number;
}
export type FormEdit = {
name?: string | null;
season?: string | null;
start?: string | null;
end?: string | null;
productor_id?: number | null;
referer_id?: number | null;
}
export type FormEditPayload = {
id: number;
form: FormEdit;
}
export type FormInputs = {
name: string;
season: string;
start: string | null;
end: string | null;
productor_id: string;
referer_id: string;
shipments: ShipmentInputs[];
}

View File

@@ -0,0 +1,33 @@
export type Productor = {
id: number;
name: string;
address: string;
payment: string;
type: string;
}
export type ProductorCreate = {
name: string;
address: string;
payment: string;
type: string;
}
export type ProductorEdit = {
name: string | null;
address: string | null;
payment: string | null;
type: string | null;
}
export type ProductorInputs = {
name: string;
address: string;
payment: string;
type: string;
}
export type ProductorEditPayload = {
productor: ProductorEdit;
id: number;
}

View File

@@ -0,0 +1,49 @@
import type { Productor } from "@/services/resources/productors";
import type { Shipment } from "@/services/resources/shipments";
export type Product = {
id: number;
productor: Productor;
name: string;
unit: number;
price: number;
priceKg: number | null;
weight: number;
type: number;
shipments: Shipment[];
}
export type ProductCreate = {
productor_id: Productor;
name: string;
unit: number;
price: number;
priceKg: number | null;
weight: number;
type: number;
}
export type ProductEdit = {
productor_id: Productor | null;
name: string | null;
unit: number | null;
price: number | null;
priceKg: number | null;
weight: number | null;
type: number | null;
}
export type ProductInputs = {
productor_id: number | null;
name: string;
unit: number | null;
price: number | null;
priceKg: number | null;
weight: number | null;
type: string;
}
export type ProductEditPayload = {
product: ProductEdit;
id: number;
}

View File

@@ -0,0 +1,30 @@
export type Shipment = {
name: string;
date: string;
id: number;
form_id: number;
}
export type ShipmentCreate = {
name: string;
date: string;
form_id: number;
}
export type ShipmentEdit = {
name: string | null;
date: string | null;
form_id: number | null;
}
export type ShipmentEditPayload = {
id: number;
shipment: ShipmentEdit;
}
export type ShipmentInputs = {
name: string | null;
date: string | null;
id: number | null;
form_id: number | null;
}

View File

@@ -0,0 +1,8 @@
import type { Product } from "@/services/resources/products";
export type User = {
id: number;
name: string;
email: string;
products: Product[];
}

View File

@@ -22,7 +22,11 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}, },
"include": ["src"] "include": ["src"]
} }

View File

@@ -3,5 +3,5 @@
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" } { "path": "./tsconfig.node.json" }
] ],
} }

View File

@@ -1,12 +1,18 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from 'path';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: { server: {
watch: { watch: {
usePolling: true, // Enable polling for file changes usePolling: true,
}, },
} }
}) })