add filters for all routes

This commit is contained in:
2026-02-15 01:09:36 +01:00
parent f440cef59e
commit a7b83da149
22 changed files with 184 additions and 82 deletions

View File

@@ -1,7 +1,11 @@
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, names: list[str], types: list[str]) -> 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: if len(names) > 0:
statement = statement.where(models.Productor.name.in_(names)) statement = statement.where(models.Productor.name.in_(names))

View File

@@ -14,7 +14,12 @@ def get_products(
types: list[str] = Query([]), types: list[str] = Query([]),
productors: list[str] = Query([]), productors: list[str] = Query([]),
): ):
return service.get_all(session, names, productors, types) return service.get_all(
session,
names,
productors,
types,
)
@router.get('/{id}', response_model=models.ProductPublic) @router.get('/{id}', response_model=models.ProductPublic)
def get_product(id: int, session: Session = Depends(get_session)): def get_product(id: int, session: Session = Depends(get_session)):

View File

@@ -6,7 +6,6 @@ def get_all(
names: list[str], names: list[str],
productors: list[str], productors: list[str],
types: list[str], types: list[str],
) -> list[models.ProductPublic]: ) -> list[models.ProductPublic]:
statement = select(models.Product) statement = select(models.Product)
if len(names) > 0: if len(names) > 0:

View File

@@ -1,8 +1,19 @@
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.ShipmentPublic]: def get_all(
session: Session,
names: list[str],
dates: list[str],
forms: list[int]
) -> list[models.ShipmentPublic]:
statement = select(models.Shipment) statement = select(models.Shipment)
if len(names) > 0:
statement = statement.where(models.Shipment.name.in_(names))
if len(dates) > 0:
statement = statement.where(models.Shipment.date.in_(list(map(lambda x: datetime.strptime(x, '%Y-%m-%d'), dates))))
if len(forms) > 0:
statement = statement.join(models.Form).where(models.Form.name.in_(forms))
return session.exec(statement.order_by(models.Shipment.name)).all() return session.exec(statement.order_by(models.Shipment.name)).all()
def get_one(session: Session, shipment_id: int) -> models.ShipmentPublic: def get_one(session: Session, shipment_id: int) -> models.ShipmentPublic:

View File

@@ -1,15 +1,26 @@
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
from sqlmodel import Session from sqlmodel import Session
import src.shipments.service as service import src.shipments.service as service
from src.auth.auth import get_current_user from src.auth.auth import get_current_user
router = APIRouter(prefix='/shipments') router = APIRouter(prefix='/shipments')
@router.get('/', response_model=list[models.ShipmentPublic], ) @router.get('/', response_model=list[models.ShipmentPublic], )
def get_shipments(session: Session = Depends(get_session)): def get_shipments(
return service.get_all(session) session: Session = Depends(get_session),
names: list[str] = Query([]),
dates: list[str] = Query([]),
forms: list[str] = Query([]),
):
return service.get_all(
session,
names,
dates,
forms,
)
@router.get('/{id}', response_model=models.ShipmentPublic) @router.get('/{id}', response_model=models.ShipmentPublic)
def get_shipment(id: int, session: Session = Depends(get_session)): def get_shipment(id: int, session: Session = Depends(get_session)):

View File

@@ -1,8 +1,16 @@
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.UserPublic]: def get_all(
session: Session,
names: list[str],
emails: list[str],
) -> list[models.UserPublic]:
statement = select(models.User) statement = select(models.User)
if len(names) > 0:
statement = statement.where(models.User.name.in_(names))
if len(emails) > 0:
statement = statement.where(models.User.email.in_(emails))
return session.exec(statement.order_by(models.User.name)).all() return session.exec(statement.order_by(models.User.name)).all()
def get_one(session: Session, user_id: int) -> models.UserPublic: def get_one(session: Session, user_id: int) -> models.UserPublic:

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,8 +8,16 @@ import src.users.service as service
router = APIRouter(prefix='/users') router = APIRouter(prefix='/users')
@router.get('/', response_model=list[models.UserPublic]) @router.get('/', response_model=list[models.UserPublic])
def get_users(session: Session = Depends(get_session)): def get_users(
return service.get_all(session) session: Session = Depends(get_session),
names: list[str] = Query([]),
emails: list[str] = Query([]),
):
return service.get_all(
session,
names,
emails,
)
@router.get('/{id}', response_model=models.UserPublic) @router.get('/{id}', response_model=models.UserPublic)
def get_users(id: int, session: Session = Depends(get_session)): def get_users(id: int, session: Session = Depends(get_session)):

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>Amap Croix-luizet</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -121,5 +121,5 @@
"error deleting shipment": "error deleting shipment", "error deleting shipment": "error deleting shipment",
"there is no contract for now": "there is no contract for now.", "there is no contract for now": "there is no contract for now.",
"the product unit will be assigned to the quantity requested in the form": "the product unit will be assigned to the quantity requested in the form", "the product unit will be assigned to the quantity requested in the form": "the product unit will be assigned to the quantity requested in the form",
"all theses informations are for contract generation, no informations is stored outside of contracts": "all theses informations are for contract generation, no informations is stored outside of contracts." "all theses informations are for contract generation": "all theses informations are for contract generation."
} }

View File

@@ -134,5 +134,5 @@
"there is no contract for now": "Il n'y a pas de contrats pour le moment.", "there is no contract for now": "Il n'y a pas de contrats pour le moment.",
"the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée a la quantité demandée dans le formulaire des amapiens.", "the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée a la quantité demandée dans le formulaire des amapiens.",
"all theses informations are for contract generation, no informations is stored outside of contracts": "ces informations sont nécéssaires pour la génération de contrat, aucune information personnelle n'est gardée ailleurs que dans les contrats générés." "all theses informations are for contract generation": "ces informations sont nécéssaires pour la génération de contrat."
} }

View File

@@ -3,11 +3,11 @@ nav {
justify-self: left; justify-self: left;
padding: 1rem; padding: 1rem;
background-color: var(--mantine-color-blue-4); background-color: var(--mantine-color-blue-4);
justify-content: space-between;
} }
.navLink { .navLink {
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
margin-right: 1rem;
text-decoration: none; text-decoration: none;
} }

View File

@@ -1,21 +1,34 @@
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";
import { Group } from "@mantine/core";
import { Config } from "@/config/config";
export function Navbar() { export function Navbar() {
return ( return (
<nav> <nav>
<Group>
<NavLink
className={"navLink"}
aria-label={t('home')}
to="/"
>
{t("home", {capfirst: true})}
</NavLink>
<NavLink
className={"navLink"}
aria-label={t('dashboard')}
to="/dashboard/productors"
>
{t("dashboard", {capfirst: true})}
</NavLink>
</Group>
<NavLink <NavLink
className={"navLink"} className={"navLink"}
to="/" aria-label={t("login with keycloak")}
to={`${Config.backend_uri}/auth/login`}
> >
{t("home", {capfirst: true})} {t("login with keycloak", {capfirst: true})}
</NavLink>
<NavLink
className={"navLink"}
to="/dashboard/productors"
>
{t("dashboard", {capfirst: true})}
</NavLink> </NavLink>
</nav> </nav>
); );

View File

@@ -24,7 +24,7 @@ export default function ProductorRow({
<Table.Td> <Table.Td>
{ {
productor.payment_methods.map((value) =>( productor.payment_methods.map((value) =>(
<Badge ml="xs"> <Badge key={value.name} ml="xs">
{t(value.name, {capfirst: true})} {t(value.name, {capfirst: true})}
</Badge> </Badge>
)) ))

View File

@@ -4,18 +4,23 @@ import { t } from "@/config/i18n";
export type ShipmentFiltersProps = { export type ShipmentFiltersProps = {
names: string[]; names: string[];
forms: string[];
filters: URLSearchParams; filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} }
export default function ShipmentsFilters({ export default function ShipmentsFilters({
names, names,
forms,
filters, filters,
onFilterChange onFilterChange
}: ShipmentFiltersProps) { }: ShipmentFiltersProps) {
const defaultNames = useMemo(() => { const defaultNames = useMemo(() => {
return filters.getAll("names") return filters.getAll("names")
}, [filters]); }, [filters]);
const defaultForms = useMemo(() => {
return filters.getAll("forms")
}, [filters]);
return ( return (
<Group> <Group>
@@ -28,6 +33,18 @@ export default function ShipmentsFilters({
onFilterChange(values, 'names') onFilterChange(values, 'names')
}} }}
clearable clearable
searchable
/>
<MultiSelect
aria-label={t("filter by form", {capfirst: true})}
placeholder={t("filter by form", {capfirst: true})}
data={forms}
defaultValue={defaultForms}
onChange={(values: string[]) => {
onFilterChange(values, 'forms')
}}
clearable
searchable
/> />
</Group> </Group>
); );

View File

@@ -44,6 +44,7 @@ export default function ShipmentModal({
const { data: allForms } = getForms(); const { data: allForms } = getForms();
const { data: allProducts } = getProducts(new URLSearchParams("types=1")); const { data: allProducts } = getProducts(new URLSearchParams("types=1"));
const { data: allProductors } = getProductors() const { data: allProductors } = getProductors()
const formsSelect = useMemo(() => { const formsSelect = useMemo(() => {
return allForms?.map(form => ({value: String(form.id), label: `${form.name} ${form.season}`})) return allForms?.map(form => ({value: String(form.id), label: `${form.name} ${form.season}`}))
}, [allForms]); }, [allForms]);

View File

@@ -80,7 +80,7 @@ export function Contract() {
shipment.products shipment.products
); );
if (total < (form?.minimum_shipment_value || 0)) { if (total < (form?.minimum_shipment_value || 0)) {
return shipment.id; // mark shipment as invalid return shipment.id;
} }
return null; return null;
}) })
@@ -125,8 +125,18 @@ export function Contract() {
} }
}, [inputForm, inputRefs, isShipmentsMinimumValue, form]); }, [inputForm, inputRefs, isShipmentsMinimumValue, form]);
if (!form) if (!form)
return <Loader/>; return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Loader color="pink"/>
</Group>
);
return ( return (
@@ -134,7 +144,7 @@ export function Contract() {
<Title order={2}>{form.name}</Title> <Title order={2}>{form.name}</Title>
<Title order={3}>{t("informations", {capfirst: true})}</Title> <Title order={3}>{t("informations", {capfirst: true})}</Title>
<Text size="sm"> <Text size="sm">
{t("all theses informations are for contract generation, no informations is stored outside of contracts", {capfirst: true})} {t("all theses informations are for contract generation", {capfirst: true})}
</Text> </Text>
<Group grow> <Group grow>
<TextInput <TextInput

View File

@@ -8,7 +8,6 @@ import FormModal from "@/components/Forms/Modal";
import FormRow from "@/components/Forms/Row"; import FormRow from "@/components/Forms/Row";
import type { Form, FormInputs } from "@/services/resources/forms"; import type { Form, FormInputs } from "@/services/resources/forms";
import FilterForms from "@/components/Forms/Filter"; import FilterForms from "@/components/Forms/Filter";
import { notifications } from "@mantine/notifications";
export function Forms() { export function Forms() {
const [ searchParams, setSearchParams ] = useSearchParams(); const [ searchParams, setSearchParams ] = useSearchParams();
@@ -59,11 +58,7 @@ export function Forms() {
minimum_shipment_value: Number(form.minimum_shipment_value), minimum_shipment_value: Number(form.minimum_shipment_value),
}); });
closeModal(); closeModal();
notifications.show({ }, [createFormMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully created form", {capfirst: true}),
});
}, [createFormMutation]);
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => { const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
if (!id) if (!id)
@@ -80,11 +75,7 @@ export function Forms() {
} }
}); });
closeModal(); closeModal();
notifications.show({ }, [editFormMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully edited form", {capfirst: true}),
});
}, [editFormMutation]);
const onFilterChange = useCallback(( const onFilterChange = useCallback((
values: string[], values: string[],
@@ -102,9 +93,19 @@ export function Forms() {
}); });
}, [searchParams, setSearchParams]) }, [searchParams, setSearchParams])
if (!data || isPending)
return (<Loader color="blue"/>);
if (!data || isPending)
return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Loader color="pink"/>
</Group>
);
return ( return (
<Stack> <Stack>
<Group justify="space-between"> <Group justify="space-between">

View File

@@ -8,7 +8,6 @@ import { ProductorModal } from "@/components/Productors/Modal";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import type { Productor, ProductorInputs } from "@/services/resources/productors"; import type { Productor, ProductorInputs } from "@/services/resources/productors";
import ProductorsFilters from "@/components/Productors/Filter"; import ProductorsFilters from "@/components/Productors/Filter";
import { notifications } from "@mantine/notifications";
export default function Productors() { export default function Productors() {
const [ searchParams, setSearchParams ] = useSearchParams(); const [ searchParams, setSearchParams ] = useSearchParams();
@@ -51,11 +50,7 @@ export default function Productors() {
...productor ...productor
}); });
closeModal(); closeModal();
notifications.show({ }, [createProductorMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully created productor", {capfirst: true}),
});
}, [createProductorMutation]);
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => { const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => {
if (!id) if (!id)
@@ -65,11 +60,7 @@ export default function Productors() {
productor: productor productor: productor
}); });
closeModal(); closeModal();
notifications.show({ }, [editProductorMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully edited productor", {capfirst: true}),
});
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => { const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => { setSearchParams(prev => {
@@ -84,7 +75,16 @@ export default function Productors() {
}, [searchParams, setSearchParams]) }, [searchParams, setSearchParams])
if (!productors || isPending) if (!productors || isPending)
return <Loader/> return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Loader color="pink"/>
</Group>
);
return ( return (
<Stack> <Stack>

View File

@@ -8,7 +8,6 @@ import { ProductModal } from "@/components/Products/Modal";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products"; import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products";
import ProductsFilters from "@/components/Products/Filter"; import ProductsFilters from "@/components/Products/Filter";
import { notifications } from "@mantine/notifications";
export default function Products() { export default function Products() {
const [ searchParams, setSearchParams ] = useSearchParams(); const [ searchParams, setSearchParams ] = useSearchParams();
@@ -49,11 +48,7 @@ export default function Products() {
const handleCreateProduct = useCallback(async (product: ProductInputs) => { const handleCreateProduct = useCallback(async (product: ProductInputs) => {
await createProductMutation.mutateAsync(productCreateFromProductInputs(product)); await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
closeModal(); closeModal();
notifications.show({ }, [createProductMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully created product", {capfirst: true}),
});
}, [createProductMutation]);
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => { const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => {
if (!id) if (!id)
@@ -63,11 +58,7 @@ export default function Products() {
product: productCreateFromProductInputs(product) product: productCreateFromProductInputs(product)
}); });
closeModal(); closeModal();
notifications.show({ }, [editProductMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully edited product", {capfirst: true}),
});
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => { const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => { setSearchParams(prev => {
@@ -82,8 +73,17 @@ export default function Products() {
}, [searchParams, setSearchParams]) }, [searchParams, setSearchParams])
if (!products || isPending) if (!products || isPending)
return <Loader/> return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Loader color="pink"/>
</Group>
);
return ( return (
<Stack> <Stack>
<Group justify="space-between"> <Group justify="space-between">

View File

@@ -8,7 +8,6 @@ import { useCallback, useMemo } from "react";
import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments"; import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
import ShipmentModal from "@/components/Shipments/Modal"; import ShipmentModal from "@/components/Shipments/Modal";
import ShipmentsFilters from "@/components/Shipments/Filter"; import ShipmentsFilters from "@/components/Shipments/Filter";
import { notifications } from "@mantine/notifications";
export default function Shipments() { export default function Shipments() {
const [ searchParams, setSearchParams ] = useSearchParams(); const [ searchParams, setSearchParams ] = useSearchParams();
@@ -38,17 +37,18 @@ export default function Shipments() {
.filter((season, index, array) => array.indexOf(season) === index) .filter((season, index, array) => array.indexOf(season) === index)
}, [allShipments]) }, [allShipments])
const forms = useMemo(() => {
return allShipments?.map((shipment: Shipment) => (shipment.form.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allShipments])
const createShipmentMutation = createShipment(); const createShipmentMutation = createShipment();
const editShipmentMutation = editShipment(); const editShipmentMutation = editShipment();
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => { const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => {
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment)); await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
closeModal(); closeModal();
notifications.show({ }, [createShipmentMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully created shipment", {capfirst: true}),
});
}, [createShipmentMutation]);
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => { const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => {
if (!id) if (!id)
@@ -58,11 +58,7 @@ export default function Shipments() {
shipment: shipmentCreateFromShipmentInputs(shipment) shipment: shipmentCreateFromShipmentInputs(shipment)
}); });
closeModal(); closeModal();
notifications.show({ }, [editShipmentMutation, closeModal]);
title: t("success", {capfirst: true}),
message: t("successfully edited shipment", {capfirst: true}),
});
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => { const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => { setSearchParams(prev => {
@@ -77,7 +73,16 @@ export default function Shipments() {
}, [searchParams, setSearchParams]) }, [searchParams, setSearchParams])
if (!shipments || isPending) if (!shipments || isPending)
return <Loader/> return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Loader color="pink"/>
</Group>
);
return ( return (
<Stack> <Stack>
@@ -106,6 +111,7 @@ export default function Shipments() {
/> />
</Group> </Group>
<ShipmentsFilters <ShipmentsFilters
forms={forms || []}
names={names || []} names={names || []}
filters={searchParams} filters={searchParams}
onFilterChange={onFilterChange} onFilterChange={onFilterChange}

View File

@@ -44,7 +44,7 @@ export default function Users() {
const handleCreateUser = useCallback(async (user: UserInputs) => { const handleCreateUser = useCallback(async (user: UserInputs) => {
await createUserMutation.mutateAsync(user); await createUserMutation.mutateAsync(user);
closeModal(); closeModal();
}, [createUserMutation]); }, [createUserMutation, closeModal]);
const handleEditUser = useCallback(async (user: UserInputs, id?: number) => { const handleEditUser = useCallback(async (user: UserInputs, id?: number) => {
if (!id) if (!id)
@@ -54,7 +54,7 @@ export default function Users() {
user: user user: user
}); });
closeModal(); closeModal();
}, []); }, [editUserMutation, closeModal]);
const onFilterChange = useCallback((values: string[], filter: string) => { const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => { setSearchParams(prev => {
@@ -69,7 +69,16 @@ export default function Users() {
}, [searchParams, setSearchParams]) }, [searchParams, setSearchParams])
if (!users || isPending) if (!users || isPending)
return <Loader/> return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Loader color="pink"/>
</Group>
);
return ( return (
<Stack> <Stack>

View File

@@ -12,7 +12,6 @@ import Users from "@/pages/Users";
import Shipments from "./pages/Shipments"; import Shipments from "./pages/Shipments";
import { Contract } from "./pages/Contract"; import { Contract } from "./pages/Contract";
import { NotFound } from "./pages/NotFound"; import { NotFound } from "./pages/NotFound";
// import { CreateForms } from "@/pages/Forms/CreateForm";
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {