diff --git a/amapcontract/shipments/Delete one.bru b/amapcontract/shipments/Delete one.bru index 789b008..fa2f55c 100644 --- a/amapcontract/shipments/Delete one.bru +++ b/amapcontract/shipments/Delete one.bru @@ -5,7 +5,7 @@ meta { } delete { - url: {{Service}}/{{Route}}/2 + url: {{Service}}/{{Route}}/1 body: none auth: inherit } diff --git a/amapcontract/users/Delete one.bru b/amapcontract/users/Delete one.bru index 789b008..fa2f55c 100644 --- a/amapcontract/users/Delete one.bru +++ b/amapcontract/users/Delete one.bru @@ -5,7 +5,7 @@ meta { } delete { - url: {{Service}}/{{Route}}/2 + url: {{Service}}/{{Route}}/1 body: none auth: inherit } diff --git a/amapcontract/users/folder.bru b/amapcontract/users/folder.bru index b74703b..005c0a2 100644 --- a/amapcontract/users/folder.bru +++ b/amapcontract/users/folder.bru @@ -8,6 +8,6 @@ auth { vars:pre-request { Route: users - ExamplePOSTBody: {"name": "test", "email": "test@test.test"} + ExamplePOSTBody: {"name": "Julien", "email": "test@test.test"} ExamplePUTBody: {"name": "updatedtest", "email": "updatedtest@test.test"} } diff --git a/backend/src/models.py b/backend/src/models.py index 320b098..6246bdc 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -104,9 +104,10 @@ class Form(FormBase, table=True): id: int | None = Field(default=None, primary_key=True) productor: Optional['Productor'] = Relationship() referer: Optional['User'] = Relationship() - shipments: list["Shipment"] = Relationship() + shipments: list["Shipment"] = Relationship(cascade_delete=True) class FormUpdate(SQLModel): + name: str | None productor_id: int | None referer_id: int | None season: str | None @@ -149,7 +150,7 @@ class ContractCreate(ContractBase): class ShipmentBase(SQLModel): name: str date: datetime.date - form_id: int | None = Field(default=None, foreign_key="form.id") + form_id: int | None = Field(default=None, foreign_key="form.id", ondelete="CASCADE") class ShipmentPublic(ShipmentBase): id: int @@ -162,7 +163,7 @@ class Shipment(ShipmentBase, table=True): class ShipmentUpdate(SQLModel): name: str | None date: str | None - product_ids: list[int] + product_ids: list[int] = [] class ShipmentCreate(ShipmentBase): product_ids: list[int] | None \ No newline at end of file diff --git a/frontend/src/components/CreateShipmentModal/index.tsx b/frontend/src/components/CreateShipmentModal/index.tsx deleted file mode 100644 index 821edf8..0000000 --- a/frontend/src/components/CreateShipmentModal/index.tsx +++ /dev/null @@ -1,56 +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 { 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/pages/Forms/FilterForms/index.tsx b/frontend/src/components/Forms/FilterForms/index.tsx similarity index 82% rename from frontend/src/pages/Forms/FilterForms/index.tsx rename to frontend/src/components/Forms/FilterForms/index.tsx index bbab236..6f79f63 100644 --- a/frontend/src/pages/Forms/FilterForms/index.tsx +++ b/frontend/src/components/Forms/FilterForms/index.tsx @@ -20,8 +20,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt return ( { @@ -30,8 +30,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt clearable /> { diff --git a/frontend/src/components/Forms/FormCard/index.tsx b/frontend/src/components/Forms/FormCard/index.tsx new file mode 100644 index 0000000..192e0b3 --- /dev/null +++ b/frontend/src/components/Forms/FormCard/index.tsx @@ -0,0 +1,77 @@ +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 ( + + {/* TODO: Show only to logged users */} + + + { + e.stopPropagation(); + navigate(`/form/${form.id}/edit`); + }} + > + + + { + deleteMutation.mutate(form.id); + }} + > + + + + + + + {form.name} + + {form.season} + + + {form.productor.name} + {form.referer.name} + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Forms/FormModal/index.tsx b/frontend/src/components/Forms/FormModal/index.tsx new file mode 100644 index 0000000..3ed5a61 --- /dev/null +++ b/frontend/src/components/Forms/FormModal/index.tsx @@ -0,0 +1,201 @@ +import { ActionIcon, Button, Collapse, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } from "@mantine/core"; +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 { 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; +} + +export type FormModalProps = ModalBaseProps & { + currentForm?: Form; + handleSubmit: (form: FormInputs, id?: number) => void; +} + +export default function FormModal({opened, onClose, currentForm, handleSubmit}: FormModalProps) { + const {data: productors} = getProductors(); + const {data: users} = getUsers(); + const form = useForm({ + initialValues: { + name: "", + season: "", + start: null, + end: null, + productor_id: "", + referer_id: "", + shipments: [], + } + }); + + useEffect(() => { + if (currentForm) { + form.initialize({ + ...currentForm, + start: currentForm.start || null, + end: currentForm.end || null, + shipments: currentForm.shipments || [], + productor_id: String(currentForm.productor.id), + referer_id: String(currentForm.referer.id) + }); + } + }, [currentForm]); + + 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 [openedShipents, { toggle: toggleShipments }] = useDisclosure(true); + + const editShipmentElement = useCallback((index: number, shipment: ShipmentInputs) => { + form.setFieldValue('shipments', (prev) => { + return prev.map((elem, id) => { + if (id === index) + return {...shipment} + return elem; + }) + }); + }, [form]) + + const deleteShipmentElement = useCallback((index: number) => { + form.setFieldValue('shipments', (prev) => { + return prev.filter((_, i) => i === index) + }); + }, [form]) + + return ( + + + + + + + + { + const target = Number(value); + form.setFieldValue('shipments', (prev) => { + const itemsToAdd = Array.from( + {length: target - prev.length}, + () => ({name: "", date: "", form_id: null, id: null}) + ); + return ( + target > prev.length ? + [...prev, ...itemsToAdd] : + prev.slice(0, target) + ); + }) + }} + /> + + {openedShipents ? : } + + + + { + form.getValues().shipments.map((value, index) => + + ) + } + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/ShipmentForm/index.tsx b/frontend/src/components/ShipmentForm/index.tsx new file mode 100644 index 0000000..9494984 --- /dev/null +++ b/frontend/src/components/ShipmentForm/index.tsx @@ -0,0 +1,63 @@ +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"; + +export type ShipmentFormProps = { + index: number; + setShipmentElement: (index: number, shipment: ShipmentInputs) => void; + deleteShipmentElement: (index: number) => void; + shipment: ShipmentInputs; +} + +export default function ShipmentForm({ + index, + setShipmentElement, + deleteShipmentElement, + shipment +}: ShipmentFormProps) { + return ( + + + { + const value = event.target.value; + setShipmentElement(index, {...shipment, name: value}) + }} + /> + { + const value = event || ""; + setShipmentElement(index, {...shipment, date: value}) + }} + /> + + + { + deleteShipmentElement(index) + }} + > + + + + + + ) +} \ No newline at end of file diff --git a/frontend/src/pages/Forms/CreateForm/index.tsx b/frontend/src/pages/Forms/CreateForm/index.tsx deleted file mode 100644 index d45bd5e..0000000 --- a/frontend/src/pages/Forms/CreateForm/index.tsx +++ /dev/null @@ -1,154 +0,0 @@ -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/ReadForm/index.tsx b/frontend/src/pages/Forms/ReadForm/index.tsx index cbddeca..d354753 100644 --- a/frontend/src/pages/Forms/ReadForm/index.tsx +++ b/frontend/src/pages/Forms/ReadForm/index.tsx @@ -5,8 +5,8 @@ import { t } from "../../../config/i18n"; import ShipmentCard from "../../../components/ShipmentCard"; export function ReadForm() { - const { isPending, error, data } = getForm(1); - console.log(isPending, error, data); + // const { isPending, error, data } = getForm(1); + // console.log(isPending, error, data); return ( { + navigate("/forms"); + }; + + const { isPending, data } = getForms(searchParams); const { data: allForms } = getForms(); const seasons = useMemo(() => { @@ -22,6 +33,77 @@ export function Forms() { .filter((productor, index, array) => array.indexOf(productor) === index) }, [allForms]) + const createFormMutation = createForm(); + const editFormMutation = editForm(); + const createShipmentsMutation = createShipment(); + const editShipmentsMutation = editShipment(); + + const handleCreateForm = useCallback(async (form: FormInputs) => { + if (!form.start || !form.end) + return; + const newForm = await createFormMutation.mutateAsync({ + ...form, + shipment_ids: [], + start: form?.start, + end: form?.start, + productor_id: Number(form.productor_id), + referer_id: Number(form.referer_id) + }); + form.shipments.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} + ); + }); + closeModal(); + }, []); + + 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) + return + const newShipment = { + name: shipment.name, + date: shipment.date, + form_id: shipment.form_id, + } + 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) => { + if (!shipment.name || !shipment.date) + return + const newShipment = { + name: shipment.name, + date: shipment.date, + } + return await createShipmentsMutation.mutateAsync( + {...newShipment, form_id: newForm.id} + ); + }); + closeModal(); + }, []); + const onFilterChange = useCallback((values: string[], filter: string) => { setSearchParams(prev => { const params = new URLSearchParams(prev); @@ -39,18 +121,21 @@ export function Forms() { return (); return ( - + {t("All forms")} { + e.stopPropagation(); + navigate(`/form/create`); + }} > + { data?.map((form: Form) => ( - - - - {form.name} - - {form.season} - - - {form.productor.name} - {form.referer.name} - - + )) } diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index def1e1d..e986acc 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -6,7 +6,6 @@ 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 { CreateForms } from "./pages/Forms/CreateForm"; export const router = createBrowserRouter([ @@ -17,8 +16,9 @@ export const router = createBrowserRouter([ children: [ { index: true, Component: Home }, { path: "/forms", Component: Forms }, - { path: "/forms/create", Component: CreateForms }, { path: "/form/:id", Component: ReadForm }, + { path: "/form/:id/edit", Component: Forms }, + { path: "/form/create", Component: Forms }, ], }, ]); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1fe2004..d4b6729 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -11,13 +11,8 @@ export type Productor = { export type Shipment = { name: string; date: string; - id: number; -} - -export type ShipmentCreate = { - name: string; - date: string; - product_ids?: number[]; + id?: number; + form_id: number; } export type Form = { @@ -38,7 +33,17 @@ export type FormCreate = { end: string; productor_id: number; referer_id: number; - shipments: Shipment[]; + 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 = { @@ -63,7 +68,8 @@ export function getForm(id: number): UseQueryResult { return useQuery
({ queryKey: ['form'], queryFn: () => ( - fetch(`${Config.backend_uri}/forms/${id}`).then((res) => res.json()) + fetch(`${Config.backend_uri}/forms/${id}`) + .then((res) => res.json()) ), }); } @@ -114,14 +120,39 @@ export function createShipment() { const queryClient = useQueryClient() return useMutation({ - mutationFn: (newShipment: ShipmentCreate) => { + mutationFn: (newShipment: Shipment) => { return fetch(`${Config.backend_uri}/shipments`, { method: 'POST', headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newShipment), - }); + // TODO change product ids hardcode here + body: JSON.stringify({...newShipment, product_ids: []}), + }).then((res) => res.json()); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['shipments'] }) + } + }) +} + +export type ShipmentEditPayload = { + id: number; + shipment: Shipment; +} + +export function editShipment() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({id, shipment}: ShipmentEditPayload) => { + return fetch(`${Config.backend_uri}/shipments/${id}`, { + method: 'PUT', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({...shipment}), + }).then((res) => res.json()); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['shipments'] }) @@ -140,7 +171,7 @@ export function createProductor() { "Content-Type": "application/json" }, body: JSON.stringify(newProductor), - }); + }).then((res) => res.json()); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['productors'] }) @@ -159,10 +190,51 @@ export function createForm() { "Content-Type": "application/json" }, body: JSON.stringify(newForm), - }); + }).then((res) => res.json()); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['forms'] }) } - }) -} \ No newline at end of file + }); +} + +export function deleteForm() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (id: number) => { + return fetch(`${Config.backend_uri}/forms/${id}`, { + method: 'DELETE', + headers: { + "Content-Type": "application/json" + }, + }).then((res) => res.json()); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['forms'] }) + } + }); +} + +export type FormEditPayload = { + id: number; + form: FormEdit; +} + +export function editForm() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({id, form}: FormEditPayload) => { + return fetch(`${Config.backend_uri}/forms/${id}`, { + method: 'PUT', + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(form), + }).then((res) => res.json()); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['forms'] }) + } + }); +}