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)
@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)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -24,6 +24,7 @@ class ProductorBase(SQLModel):
name: str
address: str
payment: str
type: str
class ProductorPublic(ProductorBase):
id: int
@@ -38,6 +39,7 @@ class ProductorUpdate(SQLModel):
name: str | None
address: str | None
payment: str | None
type: str | None
class ProductorCreate(ProductorBase):
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.models as models
from src.database import get_session
@@ -8,11 +8,15 @@ import src.productors.service as service
router = APIRouter(prefix='/productors')
@router.get('/', response_model=list[models.ProductorPublic])
def get_productors(session: Session = Depends(get_session)):
return service.get_all(session)
def get_productors(
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)
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)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)

View File

@@ -1,8 +1,12 @@
from sqlmodel import Session, select
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)
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()
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)
@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)
if result is None:
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";
export type CreateProductProps = {
@@ -11,22 +11,22 @@ export default function CreateProduct({form}: CreateProductProps) {
<Grid>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<TextInput
label={t("product name")}
placeholder={t("product name")}
label={t("product name", {capfirst: true})}
placeholder={t("product name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<NumberInput
label={t("product price")}
placeholder={t("product price")}
label={t("product price", {capfirst: true})}
placeholder={t("product price", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('price')}
/>
<TextInput
label={t("product weight")}
placeholder={t("product weight")}
label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('weight')}
@@ -34,32 +34,32 @@ export default function CreateProduct({form}: CreateProductProps) {
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<Select
label={t("product type")}
placeholder={t("product type")}
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")}
{value: "1", label: t("planned", {capfirst: true})},
{value: "2", label: t("reccurent", {capfirst: true})}
]}
defaultValue={"1"}
clearable
{...form.getInputProps('type')}
/>
<NumberInput
label={t("product price kg")}
placeholder={t("product price kg")}
label={t("product price kg", {capfirst: true})}
placeholder={t("product price kg", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('pricekg')}
{...form.getInputProps('pricekg', {capfirst: true})}
/>
<Select
label={t("product unit")}
placeholder={t("product unit")}
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")}
{value: "1", label: t("grams", {capfirst: true})},
{value: "2", label: t("kilo", {capfirst: true})},
{value: "3", label: t("piece", {capfirst: true})}
]}
defaultValue={"2"}
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 { t } from "../../../config/i18n";
import { t } from "@/config/i18n";
import { useMemo } from "react";
export type FilterFormsProps = {
@@ -9,7 +9,12 @@ export type FilterFormsProps = {
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(() => {
return filters.getAll("productors")
}, [filters]);
@@ -20,8 +25,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt
return (
<Group>
<MultiSelect
aria-label={t("filter by season")}
placeholder={t("filter by season")}
aria-label={t("filter by season", {capfirst: true})}
placeholder={t("filter by season", {capfirst: true})}
data={seasons}
defaultValue={defaultSeasons}
onChange={(values: string[]) => {
@@ -30,8 +35,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt
clearable
/>
<MultiSelect
aria-label={t("filter by productor")}
placeholder={t("filter by productor")}
aria-label={t("filter by productor", {capfirst: true})}
placeholder={t("filter by productor", {capfirst: true})}
data={productors}
defaultValue={defaultProductors}
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 { t } from "../../../config/i18n";
import { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates";
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 { useCallback, useEffect, useMemo } from "react";
import { useDisclosure } from "@mantine/hooks";
import ShipmentForm from "../../ShipmentForm";
export type FormInputs = {
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;
}
import type { Form, FormInputs } from "@/services/resources/forms";
import type { ShipmentInputs } from "@/services/resources/shipments";
import ShipmentForm from "@/components/Shipments/Form";
export type FormModalProps = ModalBaseProps & {
currentForm?: Form;
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: users} = getUsers();
const form = useForm<FormInputs>({
@@ -42,6 +32,20 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
productor_id: "",
referer_id: "",
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 editShipmentElement = useCallback((index: number, shipment: ShipmentInputs) => {
const editShipmentElement = useCallback((
index: number,
shipment: ShipmentInputs
) => {
form.setFieldValue('shipments', (prev) => {
return prev.map((elem, id) => {
if (id === index)
@@ -91,35 +98,35 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
title={currentForm ? t("edit form") : t('create form')}
>
<TextInput
label={t("form name")}
placeholder={t("form name")}
label={t("form name", {capfirst: true})}
placeholder={t("form name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<TextInput
label={t("contact season")}
placeholder={t("contact season")}
label={t("contact season", {capfirst: true})}
placeholder={t("contact season", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('season')}
/>
<DatePickerInput
label={t("start date")}
placeholder={t("start date")}
label={t("start date", {capfirst: true})}
placeholder={t("start date", {capfirst: true})}
withAsterisk
{...form.getInputProps('start')}
/>
<DatePickerInput
label={t("end date")}
placeholder={t("end date")}
label={t("end date", {capfirst: true})}
placeholder={t("end date", {capfirst: true})}
withAsterisk
{...form.getInputProps('end')}
/>
<Select
label={t("referer")}
placeholder={t("referer")}
nothingFoundMessage={t("nothing found")}
label={t("referer", {capfirst: true})}
placeholder={t("referer", {capfirst: true})}
nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk
clearable
allowDeselect
@@ -128,9 +135,9 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
{...form.getInputProps('referer_id')}
/>
<Select
label={t("productor")}
placeholder={t("productor")}
nothingFoundMessage={t("nothing found")}
label={t("productor", {capfirst: true})}
placeholder={t("productor", {capfirst: true})}
nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk
clearable
allowDeselect
@@ -140,8 +147,8 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
/>
<Group align="end">
<NumberInput
label={t("number of shipment")}
placeholder={t("number of shipment")}
label={t("number of shipment", {capfirst: true})}
placeholder={t("number of shipment", {capfirst: true})}
radius="sm"
withAsterisk
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/>}
</ActionIcon>
</Group>
@@ -183,18 +193,23 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
<Button
variant="filled"
color="red"
aria-label={t("cancel")}
aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>}
onClick={() => {
form.reset();
form.clearErrors();
onClose();
}}
>{t("cancel")}</Button>
>{t("cancel", {capfirst: true})}</Button>
<Button
variant="filled"
aria-label={currentForm ? t("edit form") : t('create form')}
onClick={() => handleSubmit(form.getValues(), currentForm?.id)}
>{currentForm ? t("edit form") : t('create form')}</Button>
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
onClick={() => {
form.validate();
if (form.isValid())
handleSubmit(form.getValues(), currentForm?.id)
}}
>{currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}</Button>
</Group>
</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 { t } from "../../config/i18n";
import { t } from "@/config/i18n";
import "./index.css";
export function Navbar() {
return (
<nav>
<NavLink to="/">{t("home")}</NavLink>
<NavLink to="/dashboard">{t("dashboard")}</NavLink>
<NavLink to="/forms">{t("forms")}</NavLink>
<NavLink to="/">{t("home", {capfirst: true})}</NavLink>
<NavLink to="/dashboard">{t("dashboard", {capfirst: true})}</NavLink>
</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 { DatePickerInput } from "@mantine/dates";
import { t } from "../../config/i18n";
import type { Shipment } from "../../services/api";
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 = {
index: number;
@@ -22,8 +21,8 @@ export default function ShipmentForm({
<Group justify="space-between" key={`shipment_${index}`}>
<Group grow maw="80%">
<TextInput
label={t("shipment name")}
placeholder={t("shipment name")}
label={t("shipment name", {capfirst: true})}
placeholder={t("shipment name", {capfirst: true})}
radius="sm"
withAsterisk
value={shipment.name || ""}
@@ -33,23 +32,23 @@ export default function ShipmentForm({
}}
/>
<DatePickerInput
label={t("shipment date")}
placeholder={t("shipment date")}
label={t("shipment date", {capfirst: true})}
placeholder={t("shipment date", {capfirst: true})}
radius="sm"
withAsterisk
value={shipment.date}
value={shipment.date || null}
onChange={(event) => {
const value = event || "";
setShipmentElement(index, {...shipment, date: value})
}}
/>
</Group>
<Tooltip label={t("remove shipment")}>
<Tooltip label={t("remove shipment", {capfirst: true})}>
<ActionIcon
flex={{base: "1", md: "0"}}
style={{alignSelf: "flex-end"}}
color="red"
aria-label={t("remove shipment")}
aria-label={t("remove shipment", {capfirst: true})}
onClick={() => {
deleteShipmentElement(index)
}}

View File

@@ -5,9 +5,9 @@ import LanguageDetector from "i18next-browser-languagedetector";
import { Settings } from "luxon";
import { initReactI18next } from "react-i18next";
import en from "../../locales/en.json";
import fr from "../../locales/fr.json";
import { Config } from "./config";
import en from "@/../locales/en.json";
import fr from "@/../locales/fr.json";
import { Config } from "@/config/config";
const resources = {
en: { translation: en },
@@ -32,10 +32,25 @@ i18next
})
.then(() => {
[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;

View File

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

View File

@@ -1,8 +1,8 @@
import { Text } from "@mantine/core";
import { t } from "../../config/i18n";
import { t } from "@/config/i18n";
export function Home() {
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 { Navbar } from "./components/Navbar";
import { Footer } from "./components/Footer";
import { Navbar } from "@/components/Navbar";
import { Footer } from "@/components/Footer";
export default function Root() {
return (

View File

@@ -2,11 +2,15 @@ import {
createBrowserRouter,
} from "react-router";
import Root from "./root";
import { Home } from "./pages/Home";
import { Forms } from "./pages/Forms";
import { ReadForm } from "./pages/Forms/ReadForm"
// import { CreateForms } from "./pages/Forms/CreateForm";
import Root from "@/root";
import { Home } from "@/pages/Home";
import { Forms } from "@/pages/Forms";
import Dashboard from "@/pages/Dashboard";
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([
{
@@ -16,9 +20,21 @@ export const router = createBrowserRouter([
children: [
{ index: true, Component: Home },
{ path: "/forms", Component: Forms },
{ path: "/form/:id", Component: ReadForm },
{ path: "/form/:id/edit", Component: Forms },
{ path: "/form/create", Component: Forms },
{ path: "/dashboard", Component: Dashboard, children: [
{path: "productors", Component: Productors},
{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 { 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 = {
id: number;
name: string;
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'],
export function getUsers() {
return useQuery<User[]>({
queryKey: ['users'],
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}` : ""}`)
fetch(`${Config.backend_uri}/users`)
.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() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (newShipment: Shipment) => {
mutationFn: (newShipment: ShipmentCreate) => {
return fetch(`${Config.backend_uri}/shipments`, {
method: 'POST',
headers: {
@@ -136,11 +46,6 @@ export function createShipment() {
})
}
export type ShipmentEditPayload = {
id: number;
shipment: Shipment;
}
export function editShipment() {
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() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (newProductor: Productor) => {
mutationFn: (newProductor: ProductorCreate) => {
return fetch(`${Config.backend_uri}/productors`, {
method: 'POST',
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() {
const queryClient = useQueryClient()
@@ -215,11 +198,6 @@ export function deleteForm() {
});
}
export type FormEditPayload = {
id: number;
form: FormEdit;
}
export function editForm() {
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,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}

View File

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

View File

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