add shipment forms and start contract from form

This commit is contained in:
2026-02-13 01:12:42 +01:00
parent fe27595931
commit ef7403f213
20 changed files with 553 additions and 312 deletions

View File

@@ -100,13 +100,13 @@ class FormPublic(FormBase):
id: int id: int
productor: ProductorPublic | None productor: ProductorPublic | None
referer: User | None referer: User | None
shipments: list["Shipment"] = [] shipments: list["ShipmentPublic"] = []
class Form(FormBase, table=True): class Form(FormBase, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
productor: Optional['Productor'] = Relationship() productor: Optional['Productor'] = Relationship()
referer: Optional['User'] = Relationship() referer: Optional['User'] = Relationship()
shipments: list["Shipment"] = Relationship(cascade_delete=True) shipments: list["Shipment"] = Relationship(back_populates="form", cascade_delete=True)
class FormUpdate(SQLModel): class FormUpdate(SQLModel):
name: str | None name: str | None
@@ -157,15 +157,18 @@ class ShipmentBase(SQLModel):
class ShipmentPublic(ShipmentBase): class ShipmentPublic(ShipmentBase):
id: int id: int
products: list[Product] = [] products: list[Product] = []
form: Form | None
class Shipment(ShipmentBase, table=True): class Shipment(ShipmentBase, table=True):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
products: list[Product] = Relationship(back_populates="shipments", link_model=ShipmentProductLink) products: list[Product] = Relationship(back_populates="shipments", link_model=ShipmentProductLink)
form: Optional[Form] = Relationship(back_populates="shipments")
class ShipmentUpdate(SQLModel): class ShipmentUpdate(SQLModel):
name: str | None name: str | None
date: str | None date: str | None
product_ids: list[int] = [] product_ids: list[int] | None = []
class ShipmentCreate(ShipmentBase): class ShipmentCreate(ShipmentBase):
product_ids: list[int] | None product_ids: list[int] = []
form_id: int

View File

@@ -1,72 +0,0 @@
import { Grid, NumberInput, Select, Stack, TextInput } from "@mantine/core";
import { t } from "../../config/i18n";
export type CreateProductProps = {
form: Record<string, any>;
}
export default function CreateProduct({form}: CreateProductProps) {
return (
<Stack>
<Grid>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<TextInput
label={t("product name", {capfirst: true})}
placeholder={t("product name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<NumberInput
label={t("product price", {capfirst: true})}
placeholder={t("product price", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('price')}
/>
<TextInput
label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('weight')}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<Select
label={t("product type", {capfirst: true})}
placeholder={t("product type", {capfirst: true})}
radius="sm"
data={[
{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", {capfirst: true})}
placeholder={t("product price kg", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('pricekg', {capfirst: true})}
/>
<Select
label={t("product unit", {capfirst: true})}
placeholder={t("product unit", {capfirst: true})}
radius="sm"
data={[
{value: "1", label: t("grams", {capfirst: true})},
{value: "2", label: t("kilo", {capfirst: true})},
{value: "3", label: t("piece", {capfirst: true})}
]}
defaultValue={"2"}
clearable
{...form.getInputProps('unit')}
/>
</Grid.Col>
</Grid>
</Stack>
);
}

View File

@@ -0,0 +1,38 @@
import { Badge, Box, Group, Paper, Text, Title } from "@mantine/core";
import { Link } from "react-router";
import type { Form } from "@/services/resources/forms";
export type FormCardProps = {
form: Form;
}
export function FormCard({form}: FormCardProps) {
return (
<Paper
shadow="xl"
p="xl"
miw={{base: "100vw", md: "25vw", lg:"20vw"}}
>
<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,14 +1,11 @@
import { ActionIcon, Button, Collapse, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } from "@mantine/core"; import { Button, Group, Modal, Select, TextInput, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { IconCancel, IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { IconCancel } from "@tabler/icons-react";
import { getProductors, getUsers } from "@/services/api"; import { getProductors, getUsers } from "@/services/api";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { useCallback, useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useDisclosure } from "@mantine/hooks";
import type { Form, FormInputs } from "@/services/resources/forms"; 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 & { export type FormModalProps = ModalBaseProps & {
currentForm?: Form; currentForm?: Form;
@@ -31,7 +28,6 @@ export default function FormModal({
end: null, end: null,
productor_id: "", productor_id: "",
referer_id: "", referer_id: "",
shipments: [],
}, },
validate: { validate: {
name: (value) => name: (value) =>
@@ -55,7 +51,6 @@ export default function FormModal({
...currentForm, ...currentForm,
start: currentForm.start || null, start: currentForm.start || null,
end: currentForm.end || null, end: currentForm.end || null,
shipments: currentForm.shipments || [],
productor_id: String(currentForm.productor.id), productor_id: String(currentForm.productor.id),
referer_id: String(currentForm.referer.id) referer_id: String(currentForm.referer.id)
}); });
@@ -70,27 +65,6 @@ export default function FormModal({
return productors?.map(prod => ({value: String(prod.id), label: `${prod.name}`})) return productors?.map(prod => ({value: String(prod.id), label: `${prod.name}`}))
}, [productors]) }, [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 ( return (
<Modal <Modal
size="50%" size="50%"
@@ -146,50 +120,6 @@ export default function FormModal({
data={productorsSelect || []} data={productorsSelect || []}
{...form.getInputProps('productor_id')} {...form.getInputProps('productor_id')}
/> />
<Group align="end">
<NumberInput
label={t("number of shipment", {capfirst: true})}
placeholder={t("number of shipment", {capfirst: true})}
radius="sm"
withAsterisk
flex="2"
value={form.getValues().shipments.length}
defaultValue={form.getValues().shipments.length}
onChange={(value: number | string) => {
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)
);
})
}}
/>
<ActionIcon
onClick={toggleShipments}
disabled={form.getValues().shipments.length === 0}
>
{openedShipents ? <IconChevronUp/> : <IconChevronDown/>}
</ActionIcon>
</Group>
<Collapse in={openedShipents}>
{
form.getValues().shipments.map((value, index) =>
<ShipmentForm
key={index}
index={index}
setShipmentElement={editShipmentElement}
deleteShipmentElement={deleteShipmentElement}
shipment={value}
/>
)
}
</Collapse>
<Group mt="sm" justify="space-between"> <Group mt="sm" justify="space-between">
<Button <Button
variant="filled" variant="filled"

View File

@@ -1,10 +1,9 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { deleteForm, getForm} from "@/services/api"; import { deleteForm} from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import type { Form, FormInputs } from "@/services/resources/forms"; import type { Form } from "@/services/resources/forms";
import FormModal from "@/components/Forms/Modal";
export type FormRowProps = { export type FormRowProps = {
form: Form; form: Form;
@@ -51,60 +50,5 @@ export default function FormRow({
</Tooltip> </Tooltip>
</Table.Td> </Table.Td>
</Table.Tr> </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

@@ -49,7 +49,7 @@ export function ProductModal({
}, [currentProduct]); }, [currentProduct]);
const productorsSelect = useMemo(() => { const productorsSelect = useMemo(() => {
return productors?.map(prod => ({value: String(prod.id), label: `${prod.name}`})) return productors?.map(productor => ({value: String(productor.id), label: `${productor.name}`}))
}, [productors]) }, [productors])
return ( return (

View File

@@ -0,0 +1,34 @@
import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react";
import { t } from "@/config/i18n";
export type ShipmentFiltersProps = {
names: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
export default function ShipmentsFilters({
names,
filters,
onFilterChange
}: ShipmentFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("names")
}, [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
/>
</Group>
);
}

View File

@@ -0,0 +1,115 @@
import { Button, Group, Modal, MultiSelect, Select, TextInput, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates";
import { IconCancel } from "@tabler/icons-react";
import { useForm } from "@mantine/form";
import { useEffect, useMemo } from "react";
import { shipmentToShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
import { getForms, getProducts } from "@/services/api";
export type ShipmentModalProps = ModalBaseProps & {
currentShipment?: Shipment;
handleSubmit: (shipment: ShipmentInputs, id?: number) => void;
}
export default function ShipmentModal({
opened,
onClose,
currentShipment,
handleSubmit
}: ShipmentModalProps) {
const form = useForm<ShipmentInputs>({
initialValues: {
name: "",
date: null,
form_id: "",
product_ids: []
},
validate: {
name: (value) =>
!value ? `${t("a name", {capfirst: true})} ${t('is required')}` : null,
date: (value) =>
!value ? `${t("a shipment date", {capfirst: true})} ${t('is required')}` : null,
form_id: (value) =>
!value ? `${t("a form", {capfirst: true})} ${t('is required')}` : null,
}
});
useEffect(() => {
if (currentShipment) {
form.setValues(shipmentToShipmentInputs(currentShipment));
}
}, [currentShipment]);
const { data: allForms } = getForms();
const { data: allProducts } = getProducts();
const formsSelect = useMemo(() => {
return allForms?.map(form => ({value: String(form.id), label: `${form.name} ${form.season}`}))
}, [allForms]);
const productsSelect = useMemo(() => {
return allProducts?.map(product => ({value: String(product.id), label: `${product.name}`}))
}, [allProducts]);
return (
<Modal
size="50%"
opened={opened}
onClose={onClose}
title={currentShipment ? t("edit shipment") : t('create shipment')}
>
<TextInput
label={t("shipment name", {capfirst: true})}
placeholder={t("shipment name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<DatePickerInput
label={t("shipment date", {capfirst: true})}
placeholder={t("shipment date", {capfirst: true})}
withAsterisk
{...form.getInputProps('date')}
/>
<Select
label={t("shipment form", {capfirst: true})}
placeholder={t("shipment form", {capfirst: true})}
radius="sm"
data={formsSelect || []}
clearable
withAsterisk
{...form.getInputProps('form_id')}
/>
<MultiSelect
label={t("shipment products", {capfirst: true})}
placeholder={t("shipment products", {capfirst: true})}
data={productsSelect}
clearable
{...form.getInputProps('product_ids')}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>}
onClick={() => {
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
<Button
variant="filled"
aria-label={currentShipment ? t("edit shipment", {capfirst: true}) : t('create shipment', {capfirst: true})}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentShipment?.id)
// form.reset();
}
}}
>{currentShipment ? t("edit shipment", {capfirst: true}) : t('create shipment', {capfirst: true})}</Button>
</Group>
</Modal>
);
}

View File

@@ -0,0 +1,51 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate } from "react-router";
import { deleteShipment} from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n";
import type { Shipment } from "@/services/resources/shipments";
export type ShipmentRowProps = {
shipment: Shipment;
}
export default function ShipmentRow({
shipment,
}: ShipmentRowProps) {
const deleteMutation = deleteShipment();
const navigate = useNavigate();
return (
<Table.Tr key={shipment.id}>
<Table.Td>{shipment.name}</Table.Td>
<Table.Td>{shipment.date}</Table.Td>
<Table.Td>{`${shipment.form.name} ${shipment.form.season}`}</Table.Td>
<Table.Td>
<Tooltip label={t("edit productor", {capfirst: true})}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/shipments/${shipment.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove productor", {capfirst: true})}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(shipment.id);
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -0,0 +1,52 @@
import { getForm } from "@/services/api";
import { Group, Loader, NumberInput, Stack, Text, Title } from "@mantine/core";
import { useMemo } from "react";
import { useParams } from "react-router";
export function Contract() {
const { id } = useParams();
const { data: form } = getForm(Number(id), {enabled: !!id})
const productsRecurent = useMemo(() => {
console.log(form)
return form?.productor?.products.filter((el) => el.type === "2")
}, [form])
const shipments = useMemo(() => {
return form?.shipments
}, [form])
if (!form)
return <Loader/>
return (
<Stack>
<Title>{form.name}</Title>
{
productsRecurent.map((el) => (
<Group>
<Text>{el.name}</Text>
<NumberInput/>
</Group>
))
}
{
shipments.map((shipment) => (
<>
<Text>{shipment.name}</Text>
{
shipment?.products.map((product) => (
<Group>
<Text>{product.name}</Text>
<NumberInput/>
</Group>
))
}
</>
))
}
</Stack>
)
}

View File

@@ -9,18 +9,18 @@ export default function Dashboard() {
return ( return (
<Tabs <Tabs
w={{base: "100%", md: "80%", lg: "60%"}} w={{base: "100%", md: "80%", lg: "60%"}}
keepMounted={false}
orientation={"horizontal"} orientation={"horizontal"}
value={location.pathname.split('/')[2]} value={location.pathname.split('/')[2]}
defaultValue={location.pathname.split('/')[2]} defaultValue={"productors"}
onChange={(value) => navigate(`/dashboard/${value}`)} onChange={(value) => navigate(`/dashboard/${value}`)}
> >
<Tabs.List> <Tabs.List>
<Tabs.Tab value="productors">{t("productors", {capfirst: true})}</Tabs.Tab> <Tabs.Tab value="productors">{t("productors", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="products">{t("products", {capfirst: true})}</Tabs.Tab> <Tabs.Tab value="products">{t("products", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="forms">{t("forms", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="shipments">{t("shipments", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="templates">{t("templates", {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="users">{t("users", {capfirst: true})}</Tabs.Tab>
<Tabs.Tab value="forms">{t("forms", {capfirst: true})}</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Outlet/> <Outlet/>
</Tabs> </Tabs>

View File

@@ -1,5 +1,5 @@
import { Stack, Loader, Title, Group, ActionIcon, Tooltip, Table, ScrollArea } from "@mantine/core"; import { Stack, Loader, Title, Group, ActionIcon, Tooltip, Table, ScrollArea } from "@mantine/core";
import { createForm, createShipment, editForm, editShipment, getForm, getForms } from "@/services/api"; import { createForm, editForm, getForm, getForms } from "@/services/api";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { IconPlus } from "@tabler/icons-react"; import { IconPlus } from "@tabler/icons-react";
@@ -7,7 +7,6 @@ import { useCallback, useMemo } from "react";
import FormModal from "@/components/Forms/Modal"; 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 type { ShipmentEdit, ShipmentInputs } from "@/services/resources/shipments";
import FilterForms from "@/components/Forms/Filter"; import FilterForms from "@/components/Forms/Filter";
export function Forms() { export function Forms() {
@@ -46,57 +45,24 @@ export function Forms() {
const createFormMutation = createForm(); const createFormMutation = createForm();
const editFormMutation = editForm(); const editFormMutation = editForm();
const createShipmentsMutation = createShipment();
const editShipmentsMutation = editShipment();
const handleCreateForm = useCallback(async (form: FormInputs) => { const handleCreateForm = useCallback(async (form: FormInputs) => {
if (!form.start || !form.end) if (!form.start || !form.end)
return; return;
const newForm = await createFormMutation.mutateAsync({ await createFormMutation.mutateAsync({
...form, ...form,
start: form?.start, start: form?.start,
end: form?.start, end: form?.start,
productor_id: Number(form.productor_id), productor_id: Number(form.productor_id),
referer_id: Number(form.referer_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(); closeModal();
}, [createFormMutation, createShipmentsMutation]); }, [createFormMutation]);
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => { const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
if (!id) if (!id)
return; return;
form.shipments await editFormMutation.mutateAsync({
.filter((el: ShipmentInputs) => el.id)
.map(async (shipment: ShipmentInputs) => {
if (
!shipment.name ||
!shipment.date ||
!shipment.form_id ||
!shipment.id
)
return
const newShipment: ShipmentEdit = {
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, id: id,
form: { form: {
...form, ...form,
@@ -106,22 +72,8 @@ export function Forms() {
referer_id: Number(form.referer_id) referer_id: Number(form.referer_id)
} }
}); });
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,
});
});
closeModal(); closeModal();
}, [editShipmentsMutation, createShipmentsMutation, editFormMutation]); }, [editFormMutation]);
const onFilterChange = useCallback(( const onFilterChange = useCallback((
values: string[], values: string[],
@@ -199,14 +151,6 @@ export function Forms() {
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</ScrollArea> </ScrollArea>
{/* <Flex gap="md" wrap="wrap" justify="center">
{
data?.map((form: Form) => (
<FormCard form={form} isEdit={isEdit} closeModal={closeModal} handleSubmit={handleEditForm}/>
))
}
</Flex> */}
</Stack> </Stack>
); );
} }

View File

@@ -1,8 +1,20 @@
import { Text } from "@mantine/core"; import { Flex } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useParams } from "react-router";
import { getForms } from "@/services/api";
import { FormCard } from "@/components/Forms/Card";
import type { Form } from "@/services/resources/forms";
export function Home() { export function Home() {
const { data: allForms } = getForms();
return ( return (
<Text>{t("test", {capfirst: true})}</Text> <Flex gap="md" wrap="wrap" justify="center">
{
allForms?.map((form: Form) => (
<FormCard form={form} key={form.id}/>
))
}
</Flex>
); );
} }

View File

@@ -0,0 +1,128 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { createShipment, editShipment, getShipment, getShipments } from "@/services/api";
import { IconPlus } from "@tabler/icons-react";
import ShipmentRow from "@/components/Shipments/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { useCallback, useMemo } from "react";
import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
import ShipmentModal from "@/components/Shipments/Modal";
import ShipmentsFilters from "@/components/Shipments/Filter";
export default function Shipments() {
const [ searchParams, setSearchParams ] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const isCreate = location.pathname === "/dashboard/shipments/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/shipments");
};
const { data: shipments, isPending } = getShipments(searchParams);
const { data: currentShipment } = getShipment(Number(editId), { enabled: !!editId });
const { data: allShipments } = getShipments();
const names = useMemo(() => {
return allShipments?.map((shipment: Shipment) => (shipment.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allShipments])
const createShipmentMutation = createShipment();
const editShipmentMutation = editShipment();
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => {
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
closeModal();
}, [createShipmentMutation]);
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => {
if (!id)
return;
await editShipmentMutation.mutateAsync({
id: id,
shipment: shipmentCreateFromShipmentInputs(shipment)
});
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 (!shipments || isPending)
return <Loader/>
return (
<Stack>
<Group justify="space-between">
<Title order={2}>{t("all shipments", {capfirst: true})}</Title>
<Tooltip label={t("create shipment", {capfirst: true})}>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/shipments/create`);
}}
>
<IconPlus/>
</ActionIcon>
</Tooltip>
<ShipmentModal
opened={isCreate}
onClose={closeModal}
handleSubmit={handleCreateShipment}
/>
<ShipmentModal
opened={isEdit}
onClose={closeModal}
currentShipment={currentShipment}
handleSubmit={handleEditShipment}
/>
</Group>
<ShipmentsFilters
names={names || []}
filters={searchParams}
onFilterChange={onFilterChange}
/>
<ScrollArea type="auto">
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("name", {capfirst: true})}</Table.Th>
<Table.Th>{t("date", {capfirst: true})}</Table.Th>
<Table.Th>{t("formulare", {capfirst: true})}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
shipments.map((shipment) => (
<ShipmentRow
shipment={shipment}
key={shipment.id}
/>
))
}
</Table.Tbody>
</Table>
</ScrollArea>
</Stack>
);
}

View File

@@ -10,6 +10,8 @@ import Productors from "@/pages/Productors";
import Products from "@/pages/Products"; import Products from "@/pages/Products";
import Templates from "@/pages/Templates"; import Templates from "@/pages/Templates";
import Users from "@/pages/Users"; import Users from "@/pages/Users";
import Shipments from "./pages/Shipments";
import { Contract } from "./pages/Contract";
// import { CreateForms } from "@/pages/Forms/CreateForm"; // import { CreateForms } from "@/pages/Forms/CreateForm";
export const router = createBrowserRouter([ export const router = createBrowserRouter([
@@ -20,7 +22,9 @@ export const router = createBrowserRouter([
children: [ children: [
{ index: true, Component: Home }, { index: true, Component: Home },
{ path: "/forms", Component: Forms }, { path: "/forms", Component: Forms },
{ path: "/dashboard", Component: Dashboard, children: [ {
path: "/dashboard", Component: Dashboard,
children: [
{ path: "productors", Component: Productors }, { path: "productors", Component: Productors },
{ path: "productors/create", Component: Productors }, { path: "productors/create", Component: Productors },
{ path: "productors/:id/edit", Component: Productors }, { path: "productors/:id/edit", Component: Productors },
@@ -34,9 +38,12 @@ export const router = createBrowserRouter([
{ path: "forms", Component: Forms }, { path: "forms", Component: Forms },
{ path: "forms/:id/edit", Component: Forms }, { path: "forms/:id/edit", Component: Forms },
{ path: "forms/create", Component: Forms }, { path: "forms/create", Component: Forms },
] }, { path: "shipments", Component: Shipments },
{ path: "shipments/:id/edit", Component: Shipments },
// { path: "/form/:id", Component: ReadForm }, { path: "shipments/create", Component: Shipments },
]
},
{ path: "/form/:id", Component: Contract},
], ],
}, },
]); ]);

View File

@@ -6,16 +6,29 @@ import type { Productor, ProductorCreate, ProductorEditPayload } from "@/service
import type { User, UserCreate, UserEditPayload } from "@/services/resources/users"; import type { User, UserCreate, UserEditPayload } from "@/services/resources/users";
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products"; import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
export function getShipments() { export function getShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> {
const queryString = filters?.toString()
return useQuery<Shipment[]>({ return useQuery<Shipment[]>({
queryKey: ['shipments'], queryKey: ['shipments', queryString],
queryFn: () => ( queryFn: () => (
fetch(`${Config.backend_uri}/shipments`) fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`)
.then((res) => res.json()) .then((res) => res.json())
), ),
}); });
} }
export function getShipment(id?: number, options?: any): UseQueryResult<Shipment, Error> {
return useQuery<Shipment>({
queryKey: ['shipment'],
queryFn: () => (
fetch(`${Config.backend_uri}/shipments/${id}`)
.then((res) => res.json())
),
enabled: !!id,
...options,
});
}
export function createShipment() { export function createShipment() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -26,8 +39,7 @@ export function createShipment() {
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
// TODO change product ids hardcode here body: JSON.stringify(newShipment),
body: JSON.stringify({...newShipment, product_ids: []}),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
@@ -40,13 +52,13 @@ export function editShipment() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationFn: ({id, shipment}: ShipmentEditPayload) => { mutationFn: ({shipment, id}: ShipmentEditPayload) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, { return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
body: JSON.stringify({...shipment}), body: JSON.stringify(shipment),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
@@ -55,6 +67,23 @@ export function editShipment() {
}) })
} }
export function deleteShipment() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: 'DELETE',
headers: {
"Content-Type": "application/json"
},
}).then((res) => res.json());
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
}
});
}
export function getProductors(filters?: URLSearchParams): UseQueryResult<Productor[], Error> { export function getProductors(filters?: URLSearchParams): UseQueryResult<Productor[], Error> {
const queryString = filters?.toString() const queryString = filters?.toString()
return useQuery<Productor[]>({ return useQuery<Productor[]>({

View File

@@ -43,5 +43,4 @@ export type FormInputs = {
end: string | null; end: string | null;
productor_id: string; productor_id: string;
referer_id: string; referer_id: string;
shipments: ShipmentInputs[];
} }

View File

@@ -1,9 +1,12 @@
import type { Product } from "./products";
export type Productor = { export type Productor = {
id: number; id: number;
name: string; name: string;
address: string; address: string;
payment: string; payment: string;
type: string; type: string;
products: Product[]
} }
export type ProductorCreate = { export type ProductorCreate = {

View File

@@ -19,42 +19,42 @@ export type Product = {
id: number; id: number;
productor: Productor; productor: Productor;
name: string; name: string;
unit: number; unit: string;
price: number; price: number;
price_kg: number | null; price_kg: number | null;
weight: number; weight: number;
type: number; type: string;
shipments: Shipment[]; shipments: Shipment[];
} }
export type ProductCreate = { export type ProductCreate = {
productor_id: number; productor_id: number;
name: string; name: string;
unit: number; unit: string;
price: number; price: number;
price_kg: number | null; price_kg: number | null;
weight: number | null; weight: number | null;
type: number; type: string;
} }
export type ProductEdit = { export type ProductEdit = {
productor_id: number | null; productor_id: number | null;
name: string | null; name: string | null;
unit: number | null; unit: string | null;
price: number | null; price: number | null;
price_kg: number | null; price_kg: number | null;
weight: number | null; weight: number | null;
type: number | null; type: string | null;
} }
export type ProductInputs = { export type ProductInputs = {
productor_id: number | null; productor_id: string | null;
name: string; name: string;
unit: number | null; unit: string | null;
price: number | null; price: number | null;
price_kg: number | null; price_kg: number | null;
weight: number | null; weight: number | null;
type: number | null; type: string | null;
} }
export type ProductEditPayload = { export type ProductEditPayload = {
@@ -64,7 +64,7 @@ export type ProductEditPayload = {
export function productToProductInputs(product: Product): ProductInputs { export function productToProductInputs(product: Product): ProductInputs {
return { return {
productor_id: product.productor.id, productor_id: String(product.productor.id),
name: product.name, name: product.name,
unit: product.unit, unit: product.unit,
price: product.price, price: product.price,
@@ -76,7 +76,7 @@ export function productToProductInputs(product: Product): ProductInputs {
export function productCreateFromProductInputs(productInput: ProductInputs): ProductCreate { export function productCreateFromProductInputs(productInput: ProductInputs): ProductCreate {
return { return {
productor_id: productInput.productor_id!, productor_id: Number(productInput.productor_id)!,
name: productInput.name, name: productInput.name,
unit: productInput.unit!, unit: productInput.unit!,
price: productInput.price!, price: productInput.price!,

View File

@@ -1,20 +1,27 @@
import type { Form } from "./forms";
import type { Product } from "./products";
export type Shipment = { export type Shipment = {
name: string; name: string;
date: string; date: string;
id: number; id: number;
form: Form;
form_id: number; form_id: number;
products: Product[];
} }
export type ShipmentCreate = { export type ShipmentCreate = {
name: string; name: string;
date: string; date: string;
form_id: number; form_id: number;
product_ids: number[];
} }
export type ShipmentEdit = { export type ShipmentEdit = {
name: string | null; name: string | null;
date: string | null; date: string | null;
form_id: number | null; form_id: number | null;
product_ids: number[];
} }
export type ShipmentEditPayload = { export type ShipmentEditPayload = {
@@ -25,6 +32,23 @@ export type ShipmentEditPayload = {
export type ShipmentInputs = { export type ShipmentInputs = {
name: string | null; name: string | null;
date: string | null; date: string | null;
id: number | null; form_id: string | null;
form_id: number | null; product_ids: string[];
}
export function shipmentToShipmentInputs(shipment: Shipment): ShipmentInputs {
return {
...shipment,
form_id: String(shipment.form_id),
product_ids: shipment.products.map((el) => (String(el.id)))
};
}
export function shipmentCreateFromShipmentInputs(shipmentInput: ShipmentInputs): ShipmentCreate {
return {
name: shipmentInput.name!,
date: shipmentInput.date!,
form_id: Number(shipmentInput.form_id),
product_ids: shipmentInput.product_ids.map(el => (Number(el))),
}
} }