From 3b2b36839ff9aed74e36403f6ef71f149289e78b Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Wed, 11 Feb 2026 00:53:40 +0100 Subject: [PATCH] [WIP] front api exchanges --- .env.example | 4 +- amapcontract/forms/folder.bru | 8 +- amapcontract/productors/folder.bru | 2 +- amapcontract/shipments/folder.bru | 2 +- backend/src/forms/forms.py | 18 +- backend/src/forms/service.py | 10 +- backend/src/models.py | 5 +- backend/src/settings.py | 2 +- .../src/components/CreateProduct/index.tsx | 72 ++++++++ .../components/CreateProductorModal/index.tsx | 62 +++++++ .../components/CreateShipmentModal/index.tsx | 56 ++++++ frontend/src/main.tsx | 3 +- frontend/src/pages/Forms/CreateForm/index.tsx | 154 ++++++++++++++++ .../src/pages/Forms/FilterForms/index.tsx | 44 +++++ .../ReadForm}/index.tsx | 12 +- frontend/src/pages/Forms/index.tsx | 92 ++++++++++ frontend/src/router.tsx | 9 +- frontend/src/services/api.ts | 166 +++++++++++++++++- frontend/vite.config.ts | 5 + 19 files changed, 694 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/CreateProduct/index.tsx create mode 100644 frontend/src/components/CreateProductorModal/index.tsx create mode 100644 frontend/src/components/CreateShipmentModal/index.tsx create mode 100644 frontend/src/pages/Forms/CreateForm/index.tsx create mode 100644 frontend/src/pages/Forms/FilterForms/index.tsx rename frontend/src/pages/{ContractForm => Forms/ReadForm}/index.tsx (92%) create mode 100644 frontend/src/pages/Forms/index.tsx diff --git a/.env.example b/.env.example index 21eede3..ba5add5 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,9 @@ DB_USER=postgres DB_PASS=postgres DB_NAME=amap DB_HOST=localhost -ORIGINS=http://localhost:8000 +ORIGINS=http://localhost:5173 SECRET_KEY= -ROOT_FQDN=http://localhost +VITE_API_URL=http://localhost:8000 KEYCLOAK_SERVER= KEYCLOAK_REALM= KEYCLOAK_CLIENT_ID= diff --git a/amapcontract/forms/folder.bru b/amapcontract/forms/folder.bru index eaa3f6c..e5f4793 100644 --- a/amapcontract/forms/folder.bru +++ b/amapcontract/forms/folder.bru @@ -10,12 +10,12 @@ vars:pre-request { Route: forms ExamplePOSTBody: ''' { - "productor_id": 1, + "productor_id": 2, "referer_id": 1, - "season": "Hiver-2026", - "shipments": 5, + "season": "Automne-2026", "start": "2026-01-10", - "end": "2026-05-10" + "end": "2026-05-10", + "name": "test very very very form long name" } ''' ExamplePUTBody: ''' diff --git a/amapcontract/productors/folder.bru b/amapcontract/productors/folder.bru index bcd5a84..c344a15 100644 --- a/amapcontract/productors/folder.bru +++ b/amapcontract/productors/folder.bru @@ -8,6 +8,6 @@ auth { vars:pre-request { Route: productors - ExamplePOSTBody: {"name": "test", "address": "test", "payment": "test"} + ExamplePOSTBody: {"name": "marie", "address": "test", "payment": "test"} ExamplePUTBody: {"name": "updatetestt", "address": "updatetestt"} } diff --git a/amapcontract/shipments/folder.bru b/amapcontract/shipments/folder.bru index ee04ee0..f95ffb7 100644 --- a/amapcontract/shipments/folder.bru +++ b/amapcontract/shipments/folder.bru @@ -13,7 +13,7 @@ vars:pre-request { "name": "test", "date": "2026-01-10", "product_ids": [1], - "form_id": 3 + "form_id": 1 } ''' ExamplePUTBody: ''' diff --git a/backend/src/forms/forms.py b/backend/src/forms/forms.py index 8e6b82a..f6cc366 100644 --- a/backend/src/forms/forms.py +++ b/backend/src/forms/forms.py @@ -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,29 +8,33 @@ import src.forms.service as service router = APIRouter(prefix='/forms') @router.get('/', response_model=list[models.FormPublic]) -def get_forms(session: Session = Depends(get_session)): - return service.get_all(session) +async def get_forms( + seasons: list[str] = Query([]), + productors: list[str] = Query([]), + session: Session = Depends(get_session) +): + return service.get_all(session, seasons, productors) @router.get('/{id}', response_model=models.FormPublic) -def get_forms(id: int, session: Session = Depends(get_session)): +async def get_forms(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) return result @router.post('/', response_model=models.FormPublic) -def create_form(form: models.FormCreate, session: Session = Depends(get_session)): +async def create_form(form: models.FormCreate, session: Session = Depends(get_session)): return service.create_one(session, form) @router.put('/{id}', response_model=models.FormPublic) -def update_form(id: int, form: models.FormUpdate, session: Session = Depends(get_session)): +async def update_form(id: int, form: models.FormUpdate, session: Session = Depends(get_session)): result = service.update_one(session, id, form) if result is None: raise HTTPException(status_code=404, detail=messages.notfound) return result @router.delete('/{id}', response_model=models.FormPublic) -def delete_form(id: int, session: Session = Depends(get_session)): +async def delete_form(id: int, session: Session = Depends(get_session)): result = service.delete_one(session, id) if result is None: raise HTTPException(status_code=404, detail=messages.notfound) diff --git a/backend/src/forms/service.py b/backend/src/forms/service.py index fe17c4e..4371e7d 100644 --- a/backend/src/forms/service.py +++ b/backend/src/forms/service.py @@ -1,8 +1,16 @@ from sqlmodel import Session, select import src.models as models -def get_all(session: Session) -> list[models.FormPublic]: +def get_all( + session: Session, + seasons: list[str], + productors: list[str] +) -> list[models.FormPublic]: statement = select(models.Form) + if len(seasons) > 0: + statement = statement.where(models.Form.season.in_(seasons)) + if len(productors) > 0: + statement = statement.join(models.Productor).where(models.Productor.name.in_(productors)) return session.exec(statement).all() def get_one(session: Session, form_id: int) -> models.FormPublic: diff --git a/backend/src/models.py b/backend/src/models.py index 6445573..320b098 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -45,6 +45,7 @@ class ProductorCreate(ProductorBase): class Unit(Enum): GRAMS = 1 KILO = 2 + PIECE = 3 class ProductType(Enum): PLANNED = 1 @@ -96,7 +97,7 @@ class FormBase(SQLModel): class FormPublic(FormBase): id: int productor: ProductorPublic | None - referer: User + referer: User | None shipments: list["Shipment"] = [] class Form(FormBase, table=True): @@ -164,4 +165,4 @@ class ShipmentUpdate(SQLModel): product_ids: list[int] class ShipmentCreate(ShipmentBase): - product_ids: list[int] \ No newline at end of file + product_ids: list[int] | None \ No newline at end of file diff --git a/backend/src/settings.py b/backend/src/settings.py index 97d5ea3..efb97f1 100644 --- a/backend/src/settings.py +++ b/backend/src/settings.py @@ -12,7 +12,7 @@ class Settings(BaseSettings): keycloak_client_id: str keycloak_client_secret: str keycloak_redirect_uri: str - root_fqdn: str + vite_api_url: str class Config: env_file = "../.env" diff --git a/frontend/src/components/CreateProduct/index.tsx b/frontend/src/components/CreateProduct/index.tsx new file mode 100644 index 0000000..1a1ff4d --- /dev/null +++ b/frontend/src/components/CreateProduct/index.tsx @@ -0,0 +1,72 @@ +import { Grid, NumberInput, Paper, Select, Stack, TextInput } from "@mantine/core"; +import { t } from "../../config/i18n"; + +export type CreateProductProps = { + form: Record; +} + +export default function CreateProduct({form}: CreateProductProps) { + return ( + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/CreateProductorModal/index.tsx b/frontend/src/components/CreateProductorModal/index.tsx new file mode 100644 index 0000000..76685d7 --- /dev/null +++ b/frontend/src/components/CreateProductorModal/index.tsx @@ -0,0 +1,62 @@ +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(); + const mutation = createProductor(); + + return ( + + {t("Informations")} + + + + {mutation.isError ? {t("an error occured")}:{mutation.error.message} : null} + {mutation.isSuccess ? {t("success")} : null} + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/CreateShipmentModal/index.tsx b/frontend/src/components/CreateShipmentModal/index.tsx new file mode 100644 index 0000000..821edf8 --- /dev/null +++ b/frontend/src/components/CreateShipmentModal/index.tsx @@ -0,0 +1,56 @@ +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 { DatePickerInput } from "@mantine/dates"; +import { createShipment, type ShipmentCreate } from "../../services/api"; +import { useEffect } from "react"; + +export function CreateShipmentModal({opened, onClose}: ModalBaseProps) { + const form = useForm(); + const mutation = createShipment(); + + return ( + + {t("informations")} + + + {mutation.isError ? {t("an error occured")}:{mutation.error.message} : null} + {mutation.isSuccess ? {t("success")} : null} + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a3c7bb3..130fd1d 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,8 +3,9 @@ import { createRoot } from "react-dom/client"; import { RouterProvider } from "react-router"; import { router } from "./router.tsx"; import { MantineProvider } from "@mantine/core"; -import '@mantine/core/styles.css'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import '@mantine/core/styles.css'; +import '@mantine/dates/styles.css'; const queryClient = new QueryClient() diff --git a/frontend/src/pages/Forms/CreateForm/index.tsx b/frontend/src/pages/Forms/CreateForm/index.tsx new file mode 100644 index 0000000..d45bd5e --- /dev/null +++ b/frontend/src/pages/Forms/CreateForm/index.tsx @@ -0,0 +1,154 @@ +import { ActionIcon, Button, Group, Loader, Modal, MultiSelect, Select, Stack, Text, TextInput, Title, Tooltip } from "@mantine/core"; +import { t } from "../../../config/i18n"; +import { DatePickerInput } from "@mantine/dates"; +import { useForm } from "@mantine/form"; +import { IconCancel, IconPlus } from "@tabler/icons-react"; +import { CreateProductorModal } from "../../../components/CreateProductorModal"; +import { useDisclosure } from "@mantine/hooks"; +import { CreateShipmentModal } from "../../../components/CreateShipmentModal"; +import { createForm, getProductors, getShipments, getUsers, type FormCreate } from "../../../services/api"; +import { useEffect, useMemo } from "react"; +import { useNavigate } from "react-router"; + +export function CreateForms() { + const form = useForm() + const navigate = useNavigate() + const [openedProductor, { open: openProductor, close: closeProductor }] = useDisclosure(false); + const [openedShipent, { open: openShipment, close: closeShipment }] = useDisclosure(false); + const {data: shipments} = getShipments(); + const {data: productors} = getProductors(); + const {data: users} = getUsers(); + const mutation = createForm(); + + const usersSelect = useMemo(() => { + return users?.map(user => ({value: String(user.id), label: `${user.name}`})) + }, [users]) + + const productorsSelect = useMemo(() => { + return productors?.map(prod => ({value: String(prod.id), label: `${prod.name}`})) + }, [productors]) + + const shipmentsSelect = useMemo(() => { + return shipments?.map(ship => ({value: String(ship.id), label: `${ship.name} ${ship.date}`})) + }, [shipments]) + + if (mutation.isSuccess) + navigate('/forms') + + return ( + + {t("create form")} + + + + + + + + + + + + + + + + + + + + + + + {mutation.isError ? {t("an error occured")}:{mutation.error.message} : null} + {mutation.isSuccess ? {t("success")} : null} + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Forms/FilterForms/index.tsx b/frontend/src/pages/Forms/FilterForms/index.tsx new file mode 100644 index 0000000..bbab236 --- /dev/null +++ b/frontend/src/pages/Forms/FilterForms/index.tsx @@ -0,0 +1,44 @@ +import { Group, MultiSelect } from "@mantine/core"; +import { t } from "../../../config/i18n"; +import { useMemo } from "react"; + +export type FilterFormsProps = { + seasons: string[]; + productors: string[]; + filters: URLSearchParams; + onFilterChange: (values: string[], filter: string) => void; +} + +export function FilterForms({seasons, productors, filters, onFilterChange}: FilterFormsProps) { + const defaultProductors = useMemo(() => { + return filters.getAll("productors") + }, [filters]); + const defaultSeasons = useMemo(() => { + return filters.getAll("seasons") + }, [filters]); + + return ( + + { + onFilterChange(values, 'seasons') + }} + clearable + /> + { + onFilterChange(values, 'productors') + }} + clearable + /> + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/ContractForm/index.tsx b/frontend/src/pages/Forms/ReadForm/index.tsx similarity index 92% rename from frontend/src/pages/ContractForm/index.tsx rename to frontend/src/pages/Forms/ReadForm/index.tsx index 197ad9e..cbddeca 100644 --- a/frontend/src/pages/ContractForm/index.tsx +++ b/frontend/src/pages/Forms/ReadForm/index.tsx @@ -1,10 +1,10 @@ import { Flex, Grid, Select, Stack, Text, TextInput, Title } from "@mantine/core"; -import { t } from "../../config/i18n"; import { IconUser } from "@tabler/icons-react"; -import ShipmentCard from "../../components/ShipmentCard"; -import { getForm } from "../../services/api"; +import { getForm } from "../../../services/api"; +import { t } from "../../../config/i18n"; +import ShipmentCard from "../../../components/ShipmentCard"; -export function ContractForm() { +export function ReadForm() { const { isPending, error, data } = getForm(1); console.log(isPending, error, data); return ( @@ -14,7 +14,7 @@ export function ContractForm() { align={"flex-start"} direction={"column"} > - + {/* {t("form contract")} {t("contract description that is rather long to show how text will be displayed even with unnecessary elements like this end of sentence")} @@ -73,7 +73,7 @@ export function ContractForm() { unit: "piece" }} /> - + */} ); } \ No newline at end of file diff --git a/frontend/src/pages/Forms/index.tsx b/frontend/src/pages/Forms/index.tsx new file mode 100644 index 0000000..65b8c68 --- /dev/null +++ b/frontend/src/pages/Forms/index.tsx @@ -0,0 +1,92 @@ +import { Stack, Loader, Text, Title, Paper, Group, Badge, ActionIcon, Grid, Flex, Select, MultiSelect, Tooltip } from "@mantine/core"; +import { getForms, type Form } from "../../services/api"; +import { t } from "../../config/i18n"; +import { Link, useSearchParams } from "react-router"; +import { IconPlus } from "@tabler/icons-react"; +import { useCallback, useMemo } from "react"; +import { FilterForms } from "./FilterForms"; + +export function Forms() { + const [ searchParams, setSearchParams ] = useSearchParams(); + + const { isPending, error, data } = getForms(searchParams); + const { data: allForms } = getForms(); + + const seasons = useMemo(() => { + return allForms?.map((form: Form) => (form.season)) + .filter((season, index, array) => array.indexOf(season) === index) + }, [allForms]) + + const productors = useMemo(() => { + return allForms?.map((form: Form) => (form.productor.name)) + .filter((productor, index, array) => array.indexOf(productor) === index) + }, [allForms]) + + 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 (!data || isPending) + return (); + + return ( + + + {t("All forms")} + + + + + + + + + { + data?.map((form: Form) => ( + + + + {form.name} + + {form.season} + + + {form.productor.name} + {form.referer.name} + + + )) + } + + + ); +} \ No newline at end of file diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 466b5a6..def1e1d 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -4,7 +4,10 @@ import { import Root from "./root"; import { Home } from "./pages/Home"; -import { ContractForm } from "./pages/ContractForm"; +import { Forms } from "./pages/Forms"; +import { ReadForm } from "./pages/Forms/ReadForm" +import { CreateForms } from "./pages/Forms/CreateForm"; +// import { CreateForms } from "./pages/Forms/CreateForm"; export const router = createBrowserRouter([ { @@ -13,7 +16,9 @@ export const router = createBrowserRouter([ // errorElement: , children: [ { index: true, Component: Home }, - { path: "/forms", Component: ContractForm }, + { path: "/forms", Component: Forms }, + { path: "/forms/create", Component: CreateForms }, + { path: "/form/:id", Component: ReadForm }, ], }, ]); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 8c3c4ec..1fe2004 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,10 +1,168 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient,type UseQueryResult } from "@tanstack/react-query"; +import { Config } from "../config/config"; -export function getForm(id: number) { - return useQuery({ +export type Productor = { + id: number; + name: string; + address: string; + payment: string; +} + +export type Shipment = { + name: string; + date: string; + id: number; +} + +export type ShipmentCreate = { + name: string; + date: string; + product_ids?: 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; + shipments: 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 { + return useQuery
({ queryKey: ['form'], queryFn: () => ( - fetch(`http://localhost:8000/forms/${id}`).then((res) => res.json()) + fetch(`${Config.backend_uri}/forms/${id}`).then((res) => res.json()) ), }); +} + +export function getForms(filters?: URLSearchParams): UseQueryResult { + const queryString = filters?.toString() + return useQuery({ + queryKey: ['forms', queryString], + queryFn: () => ( + fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`) + .then((res) => res.json()) + ), + }); +} + +export function getShipments() { + return useQuery({ + queryKey: ['shipments'], + queryFn: () => ( + fetch(`${Config.backend_uri}/shipments`) + .then((res) => res.json()) + ), + }); +} + +export function getProductors() { + return useQuery({ + queryKey: ['productors'], + queryFn: () => ( + fetch(`${Config.backend_uri}/productors`) + .then((res) => res.json()) + ), + }); +} + + +export function getUsers() { + return useQuery({ + queryKey: ['users'], + queryFn: () => ( + fetch(`${Config.backend_uri}/users`) + .then((res) => res.json()) + ), + }); +} + +export function createShipment() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (newShipment: ShipmentCreate) => { + return fetch(`${Config.backend_uri}/shipments`, { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(newShipment), + }); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['shipments'] }) + } + }) +} + +export function createProductor() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (newProductor: Productor) => { + return fetch(`${Config.backend_uri}/productors`, { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(newProductor), + }); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['productors'] }) + } + }) +} + +export function createForm() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (newForm: FormCreate) => { + return fetch(`${Config.backend_uri}/forms`, { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(newForm), + }); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['forms'] }) + } + }) } \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b0f57b..600a49e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,4 +4,9 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + watch: { + usePolling: true, // Enable polling for file changes + }, + } })