From fe2759593126977ef0587e1943b99f09600d2265 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Thu, 12 Feb 2026 19:10:50 +0100 Subject: [PATCH] add users and fix modal --- frontend/src/components/Forms/Modal/index.tsx | 7 +- frontend/src/components/Forms/Row/index.tsx | 15 +-- .../components/Productors/Filter/index.tsx | 1 + .../src/components/Productors/Modal/index.tsx | 9 +- .../src/components/Productors/Row/index.tsx | 18 +-- .../src/components/Products/Filter/index.tsx | 1 + .../src/components/Products/Modal/index.tsx | 6 +- .../src/components/Products/Row/index.tsx | 13 -- .../src/components/Users/Filter/index.tsx | 34 +++++ frontend/src/components/Users/Modal/index.tsx | 87 ++++++++++++ frontend/src/components/Users/Row/index.tsx | 52 ++++++++ frontend/src/pages/Forms/index.tsx | 21 ++- frontend/src/pages/Productors/index.tsx | 27 ++-- frontend/src/pages/Products/index.tsx | 26 ++-- frontend/src/pages/Users/index.tsx | 125 +++++++++++++++++- frontend/src/router.tsx | 2 + frontend/src/services/api.ts | 106 ++++++++++++--- frontend/src/services/resources/users.ts | 20 +++ 18 files changed, 482 insertions(+), 88 deletions(-) create mode 100644 frontend/src/components/Users/Filter/index.tsx create mode 100644 frontend/src/components/Users/Modal/index.tsx create mode 100644 frontend/src/components/Users/Row/index.tsx diff --git a/frontend/src/components/Forms/Modal/index.tsx b/frontend/src/components/Forms/Modal/index.tsx index 4dcca40..5fea59c 100644 --- a/frontend/src/components/Forms/Modal/index.tsx +++ b/frontend/src/components/Forms/Modal/index.tsx @@ -51,7 +51,7 @@ export default function FormModal({ useEffect(() => { if (currentForm) { - form.initialize({ + form.setValues({ ...currentForm, start: currentForm.start || null, end: currentForm.end || null, @@ -93,6 +93,7 @@ export default function FormModal({ return ( { form.validate(); - if (form.isValid()) + if (form.isValid()) { handleSubmit(form.getValues(), currentForm?.id) + form.reset(); + } }} >{currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})} diff --git a/frontend/src/components/Forms/Row/index.tsx b/frontend/src/components/Forms/Row/index.tsx index 8a0b936..e0d0840 100644 --- a/frontend/src/components/Forms/Row/index.tsx +++ b/frontend/src/components/Forms/Row/index.tsx @@ -8,29 +8,16 @@ 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 ( - {form.name} {form.season} {form.start} diff --git a/frontend/src/components/Productors/Filter/index.tsx b/frontend/src/components/Productors/Filter/index.tsx index 8bc9e04..15df3f6 100644 --- a/frontend/src/components/Productors/Filter/index.tsx +++ b/frontend/src/components/Productors/Filter/index.tsx @@ -21,6 +21,7 @@ export default function ProductorsFilter({ const defaultTypes = useMemo(() => { return filters.getAll("types") }, [filters]); + return ( { if (currentProductor) { - form.initialize({ + form.setValues({ ...currentProductor, }); - } + } }, [currentProductor]); return ( @@ -99,6 +99,7 @@ export function ProductorModal({ form.validate(); if (form.isValid()) { handleSubmit(form.getValues(), currentProductor?.id) + form.reset(); } }} >{currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})} diff --git a/frontend/src/components/Productors/Row/index.tsx b/frontend/src/components/Productors/Row/index.tsx index bf8255b..00eb470 100644 --- a/frontend/src/components/Productors/Row/index.tsx +++ b/frontend/src/components/Productors/Row/index.tsx @@ -1,36 +1,22 @@ 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 type { Productor } from "@/services/resources/productors"; +import { deleteProductor } 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 ( - {productor.name} {productor.type} {productor.address} diff --git a/frontend/src/components/Products/Filter/index.tsx b/frontend/src/components/Products/Filter/index.tsx index b00a672..91aeb7f 100644 --- a/frontend/src/components/Products/Filter/index.tsx +++ b/frontend/src/components/Products/Filter/index.tsx @@ -21,6 +21,7 @@ export default function ProductsFilters({ const defaultProductors = useMemo(() => { return filters.getAll("productors") }, [filters]); + return ( { if (currentProduct) { - form.initialize(productToProductInputs(currentProduct)); + form.setValues(productToProductInputs(currentProduct)); } }, [currentProduct]); @@ -136,8 +136,10 @@ export function ProductModal({ onClick={() => { form.validate(); console.log(form.isValid(), form.getValues()) - if (form.isValid()) + if (form.isValid()) { handleSubmit(form.getValues(), currentProduct?.id) + form.reset(); + } }} >{currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})} diff --git a/frontend/src/components/Products/Row/index.tsx b/frontend/src/components/Products/Row/index.tsx index 242bc43..9c94b1b 100644 --- a/frontend/src/components/Products/Row/index.tsx +++ b/frontend/src/components/Products/Row/index.tsx @@ -8,29 +8,16 @@ 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 ( - {product.name} {ProductType[product.type]} {product.price} diff --git a/frontend/src/components/Users/Filter/index.tsx b/frontend/src/components/Users/Filter/index.tsx new file mode 100644 index 0000000..440af01 --- /dev/null +++ b/frontend/src/components/Users/Filter/index.tsx @@ -0,0 +1,34 @@ +import { Group, MultiSelect } from "@mantine/core"; +import { useMemo } from "react"; +import { t } from "@/config/i18n"; + +export type UserFiltersProps = { + names: string[]; + filters: URLSearchParams; + onFilterChange: (values: string[], filter: string) => void; +} + +export default function UserFilters({ + names, + filters, + onFilterChange +}: UserFiltersProps) { + const defaultNames = useMemo(() => { + return filters.getAll("names") + }, [filters]); + + return ( + + { + onFilterChange(values, 'names') + }} + clearable + /> + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Users/Modal/index.tsx b/frontend/src/components/Users/Modal/index.tsx new file mode 100644 index 0000000..b78ff70 --- /dev/null +++ b/frontend/src/components/Users/Modal/index.tsx @@ -0,0 +1,87 @@ +import { Button, Group, Modal, 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 User, type UserInputs } from "@/services/resources/users"; +import { useEffect } from "react"; + +export type UserModalProps = ModalBaseProps & { + currentUser?: User; + handleSubmit: (user: UserInputs, id?: number) => void; +} + +export function UserModal({ + opened, + onClose, + currentUser, + handleSubmit +}: UserModalProps) { + const form = useForm({ + initialValues: { + name: "", + email: "" + }, + validate: { + name: (value) => + !value ? `${t("name", {capfirst: true})} ${t('is required')}` : null, + email: (value) => + !value ? `${t("email", {capfirst: true})} ${t('is required')}` : null, + } + }); + + useEffect(() => { + if (currentUser) { + form.setValues(currentUser); + } + }, [currentUser]); + + return ( + + {t("informations", {capfirst: true})} + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Users/Row/index.tsx b/frontend/src/components/Users/Row/index.tsx new file mode 100644 index 0000000..c21d9e4 --- /dev/null +++ b/frontend/src/components/Users/Row/index.tsx @@ -0,0 +1,52 @@ +import { ActionIcon, Table, Tooltip } from "@mantine/core"; +import { t } from "@/config/i18n"; +import { IconEdit, IconX } from "@tabler/icons-react"; +import { type User, type UserInputs } from "@/services/resources/users"; +import { UserModal } from "@/components/Users/Modal"; +import { deleteUser, getUser } from "@/services/api"; +import { useNavigate } from "react-router"; + +export type UserRowProps = { + user: User; +} + +export default function UserRow({ + user, +}: UserRowProps) { + const deleteMutation = deleteUser(); + const navigate = useNavigate(); + + return ( + + {user.name} + {user.email} + + + { + e.stopPropagation(); + navigate(`/dashboard/users/${user.id}/edit`); + }} + > + + + + + { + deleteMutation.mutate(user.id); + }} + > + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Forms/index.tsx b/frontend/src/pages/Forms/index.tsx index b759c8d..3901cc3 100644 --- a/frontend/src/pages/Forms/index.tsx +++ b/frontend/src/pages/Forms/index.tsx @@ -1,5 +1,5 @@ import { Stack, Loader, Title, Group, ActionIcon, Tooltip, Table, ScrollArea } from "@mantine/core"; -import { createForm, createShipment, editForm, editShipment, getForms } from "@/services/api"; +import { createForm, createShipment, editForm, editShipment, getForm, getForms } from "@/services/api"; import { t } from "@/config/i18n"; import { useLocation, useNavigate, useSearchParams } from "react-router"; import { IconPlus } from "@tabler/icons-react"; @@ -18,11 +18,20 @@ export function Forms() { const isCreate = location.pathname === "/dashboard/forms/create"; const isEdit = location.pathname.includes("/edit"); + const editId = useMemo(() => { + if (isEdit) { + return location.pathname.split("/")[3] + } + return null + }, [location, isEdit]) + const closeModal = () => { navigate("/dashboard/forms"); }; const { isPending, data } = getForms(searchParams); + const { data: currentForm } = getForm(Number(editId), { enabled: !!editId }); + const { data: allForms } = getForms(); const seasons = useMemo(() => { @@ -159,6 +168,12 @@ export function Forms() { filters={searchParams} onFilterChange={onFilterChange} /> + @@ -177,9 +192,7 @@ export function Forms() { data.map((form) => ( )) } diff --git a/frontend/src/pages/Productors/index.tsx b/frontend/src/pages/Productors/index.tsx index 2baedc6..05cc069 100644 --- a/frontend/src/pages/Productors/index.tsx +++ b/frontend/src/pages/Productors/index.tsx @@ -1,11 +1,11 @@ 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 { createProductor, editProductor, getProductor, 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 { useCallback, useMemo, useState } from "react"; import type { Productor, ProductorInputs } from "@/services/resources/productors"; import ProductorsFilters from "@/components/Productors/Filter"; @@ -17,12 +17,20 @@ export default function Productors() { const isCreate = location.pathname === "/dashboard/productors/create"; const isEdit = location.pathname.includes("/edit"); + const editId = useMemo(() => { + if (isEdit) { + return location.pathname.split("/")[3] + } + return null + }, [location, isEdit]) + const closeModal = () => { navigate("/dashboard/productors"); }; - const {data: productors, isPending} = getProductors(searchParams); - const {data: allProductors } = getProductors(); + const { data: productors, isPending } = getProductors(searchParams); + const { data: currentProductor } = getProductor(Number(editId), { enabled: !!editId }); + const { data: allProductors } = getProductors(); const names = useMemo(() => { return allProductors?.map((productor: Productor) => (productor.name)) @@ -88,6 +96,12 @@ export default function Productors() { onClose={closeModal} handleSubmit={handleCreateProductor} /> + ( )) } diff --git a/frontend/src/pages/Products/index.tsx b/frontend/src/pages/Products/index.tsx index 6a56470..4dfb54c 100644 --- a/frontend/src/pages/Products/index.tsx +++ b/frontend/src/pages/Products/index.tsx @@ -1,6 +1,6 @@ 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 { createProduct, editProduct, getProduct, getProducts } from "@/services/api"; import { IconPlus } from "@tabler/icons-react"; import ProductRow from "@/components/Products/Row"; import { useLocation, useNavigate, useSearchParams } from "react-router"; @@ -17,12 +17,20 @@ export default function Products() { const isCreate = location.pathname === "/dashboard/products/create"; const isEdit = location.pathname.includes("/edit"); + const editId = useMemo(() => { + if (isEdit) { + return location.pathname.split("/")[3] + } + return null + }, [location, isEdit]) + const closeModal = () => { navigate("/dashboard/products"); }; - const {data: products, isPending} = getProducts(searchParams); - const {data: allProducts } = getProducts(); + const { data: products, isPending } = getProducts(searchParams); + const { data: currentProduct } = getProduct(Number(editId), { enabled: !!editId }); + const { data: allProducts } = getProducts(); const names = useMemo(() => { return allProducts?.map((product: Product) => (product.name)) @@ -60,7 +68,6 @@ export default function Products() { values.forEach(value => { params.append(filter, value); }); - return params; }); }, [searchParams, setSearchParams]) @@ -94,6 +101,12 @@ export default function Products() { filters={searchParams} onFilterChange={onFilterChange} /> +
@@ -112,10 +125,7 @@ export default function Products() { products.map((product) => ( )) } diff --git a/frontend/src/pages/Users/index.tsx b/frontend/src/pages/Users/index.tsx index 4eaab83..6a91623 100644 --- a/frontend/src/pages/Users/index.tsx +++ b/frontend/src/pages/Users/index.tsx @@ -1,5 +1,128 @@ +import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core"; +import { t } from "@/config/i18n"; +import { createUser, editUser, getUser, getUsers } from "@/services/api"; +import { IconPlus } from "@tabler/icons-react"; +import UserRow from "@/components/Users/Row"; +import { useLocation, useNavigate, useSearchParams } from "react-router"; +import { UserModal } from "@/components/Users/Modal"; +import { useCallback, useMemo } from "react"; +import { type User, type UserInputs } from "@/services/resources/users"; +import UsersFilters from "@/components/Users/Filter"; + export default function Users() { + const [ searchParams, setSearchParams ] = useSearchParams(); + const location = useLocation(); + const navigate = useNavigate(); + + const isCreate = location.pathname === "/dashboard/users/create"; + const isEdit = location.pathname.includes("/edit"); + + const editId = useMemo(() => { + if (isEdit) { + return location.pathname.split("/")[3] + } + return null + }, [location, isEdit]) + + const closeModal = () => { + navigate("/dashboard/users"); + }; + + const {data: users, isPending} = getUsers(searchParams); + const { data: currentUser } = getUser(Number(editId), { enabled: !!editId }); + + const {data: allUsers } = getUsers(); + + const names = useMemo(() => { + return allUsers?.map((user: User) => (user.name)) + .filter((season, index, array) => array.indexOf(season) === index) + }, [allUsers]) + + const createUserMutation = createUser(); + const editUserMutation = editUser(); + + const handleCreateUser = useCallback(async (user: UserInputs) => { + await createUserMutation.mutateAsync(user); + closeModal(); + }, [createUserMutation]); + + const handleEditUser = useCallback(async (user: UserInputs, id?: number) => { + if (!id) + return; + await editUserMutation.mutateAsync({ + id: id, + user: user + }); + 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 (!users || isPending) + return + return ( - <> + + + {t("all users", {capfirst: true})} + + { + e.stopPropagation(); + navigate(`/dashboard/users/create`); + }} + > + + + + + + + + +
+ + + {t("name", {capfirst: true})} + {t("email", {capfirst: true})} + {t("actions", {capfirst: true})} + + + + { + users.map((user) => ( + + )) + } + +
+
+ ); } \ No newline at end of file diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 8d530f2..3325148 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -29,6 +29,8 @@ export const router = createBrowserRouter([ {path: "products/:id/edit", Component: Products}, { path: "templates", Component: Templates }, { path: "users", Component: Users }, + {path: "users/create", Component: Users}, + {path: "users/:id/edit", Component: Users}, { path: "forms", Component: Forms }, { path: "forms/:id/edit", Component: Forms }, { path: "forms/create", Component: Forms }, diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 9adc0c0..6266506 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -3,19 +3,9 @@ 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 { User, UserCreate, UserEditPayload } from "@/services/resources/users"; import type { Product, ProductCreate, ProductEditPayload } from "./resources/products"; -export function getUsers() { - return useQuery({ - queryKey: ['users'], - queryFn: () => ( - fetch(`${Config.backend_uri}/users`) - .then((res) => res.json()) - ), - }); -} - export function getShipments() { return useQuery({ queryKey: ['shipments'], @@ -65,10 +55,10 @@ export function editShipment() { }) } -export function getProductors(filters?: URLSearchParams) { +export function getProductors(filters?: URLSearchParams): UseQueryResult { const queryString = filters?.toString() return useQuery({ - queryKey: ['productors', filters], + queryKey: ['productors', queryString], queryFn: () => ( fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`) .then((res) => res.json()) @@ -76,13 +66,15 @@ export function getProductors(filters?: URLSearchParams) { }); } -export function getProductor(id: number) { +export function getProductor(id?: number, options?: any) { return useQuery({ queryKey: ['productor'], queryFn: () => ( fetch(`${Config.backend_uri}/productors/${id}`) .then((res) => res.json()) ), + enabled: !!id, + ...options, }); } @@ -141,13 +133,15 @@ export function deleteProductor() { }); } -export function getForm(id: number): UseQueryResult { +export function getForm(id?: number, options?: any) { return useQuery
({ queryKey: ['form'], queryFn: () => ( fetch(`${Config.backend_uri}/forms/${id}`) .then((res) => res.json()) ), + enabled: !!id, + ...options, }); } @@ -217,13 +211,15 @@ export function editForm() { }); } -export function getProduct(id: number): UseQueryResult { +export function getProduct(id?: number, options?: any) { return useQuery({ queryKey: ['product'], queryFn: () => ( fetch(`${Config.backend_uri}/products/${id}`) .then((res) => res.json()) ), + enabled: !!id, + ...options, }); } @@ -292,3 +288,81 @@ export function editProduct() { } }); } + +export function getUser(id?: number, options?: any) { + return useQuery({ + queryKey: ['user'], + queryFn: () => ( + fetch(`${Config.backend_uri}/users/${id}`) + .then((res) => res.json()) + ), + enabled: !!id, + ...options, + }); +} + +export function getUsers(filters?: URLSearchParams): UseQueryResult { + const queryString = filters?.toString() + return useQuery({ + queryKey: ['users', queryString], + queryFn: () => ( + fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`) + .then((res) => res.json()) + ), + }); +} + +export function createUser() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (newUser: UserCreate) => { + return fetch(`${Config.backend_uri}/users`, { + method: 'POST', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(newUser), + }).then((res) => res.json()); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['users'] }) + } + }); +} + +export function deleteUser() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (id: number) => { + return fetch(`${Config.backend_uri}/users/${id}`, { + method: 'DELETE', + headers: { + "Content-Type": "application/json" + }, + }).then((res) => res.json()); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['users'] }) + } + }); +} + +export function editUser() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({id, user}: UserEditPayload) => { + return fetch(`${Config.backend_uri}/users/${id}`, { + method: 'PUT', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(user), + }).then((res) => res.json()); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['users'] }) + } + }); +} diff --git a/frontend/src/services/resources/users.ts b/frontend/src/services/resources/users.ts index ffc1f32..e96d64a 100644 --- a/frontend/src/services/resources/users.ts +++ b/frontend/src/services/resources/users.ts @@ -6,3 +6,23 @@ export type User = { email: string; products: Product[]; } + +export type UserInputs = { + email: string; + name: string; +} + +export type UserCreate ={ + email: string | null; + name: string | null; +} + +export type UserEdit ={ + email: string | null; + name: string | null; +} + +export type UserEditPayload = { + user: UserEdit; + id: number; +} \ No newline at end of file