add contract pdf generation

This commit is contained in:
2026-02-14 23:59:44 +01:00
parent 7e42fbe106
commit f440cef59e
42 changed files with 1299 additions and 123 deletions

View File

@@ -1,12 +1,12 @@
import { ProductForm } from "@/components/Products/Form";
import ShipmentForm from "@/components/Shipments/Form";
import { t } from "@/config/i18n";
import { getForm } from "@/services/api";
import { createContract, getForm } from "@/services/api";
import { type Product } from "@/services/resources/products";
import { Accordion, Button, Group, List, Loader, Overlay, Stack, Text, TextInput, Title } from "@mantine/core";
import { useForm } from "@mantine/form";
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
import { useMemo } from "react";
import { useCallback, useMemo, useRef } from "react";
import { useParams } from "react-router";
export function computePrices(values: [string, any][], products: Product[], nbShipment?: number) {
@@ -15,7 +15,7 @@ export function computePrices(values: [string, any][], products: Product[], nbSh
const productId = Number(keyArray[keyArray.length - 1]);
const product = products.find((product) => product.id === productId);
if (!product) {
return 0;
return prev + 0;
}
const isRecurent = key.includes("recurrent") && nbShipment;
const productPrice = Number(product.price || product.price_kg);
@@ -29,6 +29,12 @@ export function Contract() {
const { id } = useParams();
const { data: form } = getForm(Number(id), {enabled: !!id});
const inputForm = useForm<Record<string, number | string>>({
initialValues: {
firstname: "",
lastname: "",
email: "",
phone: "",
},
validate: {
firstname: (value) => !value ? `${t("a firstname", {capfirst: true})} ${t("is required")}` : null,
lastname: (value) => !value ? `${t("a lastname", {capfirst: true})} ${t("is required")}` : null,
@@ -37,6 +43,8 @@ export function Contract() {
}
});
const createContractMutation = createContract();
const productsRecurent = useMemo(() => {
return form?.productor?.products.filter((el) => el.type === "2")
}, [form]);
@@ -50,10 +58,73 @@ export function Contract() {
}, [form])
const price = useMemo(() => {
if (!allProducts) {
return 0;
}
const values = Object.entries(inputForm.getValues());
return computePrices(values, allProducts, form?.shipments.length);
}, [inputForm, allProducts, form?.shipments]);
const inputRefs: Record<string, React.RefObject<HTMLInputElement | null>> = {
firstname: useRef<HTMLInputElement>(null),
lastname: useRef<HTMLInputElement>(null),
email: useRef<HTMLInputElement>(null),
phone: useRef<HTMLInputElement>(null)
}
const isShipmentsMinimumValue = useCallback(() => {
const shipmentErrors = form.shipments
.map((shipment) => {
const total = computePrices(
Object.entries(inputForm.getValues()),
shipment.products
);
if (total < (form?.minimum_shipment_value || 0)) {
return shipment.id; // mark shipment as invalid
}
return null;
})
.filter(Boolean);
return shipmentErrors.length === 0;
}, [form]);
const withDefaultValues = useCallback((values: Record<string, number | string>) => {
const result = {...values};
productsRecurent.forEach((product: Product) => {
const key = `recurrent-${product.id}`;
if (result[key] === undefined || result[key] === "") {
result[key] = 0;
}
});
form.shipments.forEach((shipment) => {
shipment.products.forEach((product) => {
const key = `planned-${shipment.id}-${product.id}`;
if (result[key] === undefined || result[key] === "") {
result[key] = 0;
}
})
});
return result;
}, [productsRecurent, form]);
const handleSubmit = useCallback(async () => {
const errors = inputForm.validate();
if (inputForm.isValid() && isShipmentsMinimumValue()) {
const contract = {
form_id: form.id,
contract: withDefaultValues(inputForm.getValues()),
}
await createContractMutation.mutateAsync(contract);
} else {
const firstErrorField = Object.keys(errors.errors)[0];
const ref = inputRefs[firstErrorField];
ref?.current?.scrollIntoView({behavior: "smooth", block: "center"});
}
}, [inputForm, inputRefs, isShipmentsMinimumValue, form]);
if (!form)
return <Loader/>;
@@ -74,6 +145,7 @@ export function Contract() {
required
leftSection={<IconUser/>}
{...inputForm.getInputProps('firstname')}
ref={inputRefs.firstname}
/>
<TextInput
label={t("lastname", {capfirst: true})}
@@ -83,6 +155,7 @@ export function Contract() {
required
leftSection={<IconUser/>}
{...inputForm.getInputProps('lastname')}
ref={inputRefs.lastname}
/>
</Group>
<Group grow>
@@ -94,6 +167,7 @@ export function Contract() {
required
leftSection={<IconMail/>}
{...inputForm.getInputProps('email')}
ref={inputRefs.email}
/>
<TextInput
label={t("phone", {capfirst: true})}
@@ -103,6 +177,7 @@ export function Contract() {
required
leftSection={<IconPhone/>}
{...inputForm.getInputProps('phone')}
ref={inputRefs.phone}
/>
</Group>
<Title order={3}>{t('shipments', {capfirst: true})}</Title>
@@ -149,6 +224,7 @@ export function Contract() {
{
shipments.map((shipment, index) => (
<ShipmentForm
minimumPrice={form.minimum_shipment_value}
shipment={shipment}
index={index}
inputForm={inputForm}
@@ -180,10 +256,7 @@ export function Contract() {
</Text>
<Button
aria-label={t('submit contract')}
onClick={() => {
inputForm.validate();
console.log(inputForm.getValues())
}}
onClick={handleSubmit}
>
{t('submit contract')}
</Button>

View File

@@ -19,7 +19,7 @@ export default function Dashboard() {
<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.List>
<Outlet/>

View File

@@ -8,6 +8,7 @@ import FormModal from "@/components/Forms/Modal";
import FormRow from "@/components/Forms/Row";
import type { Form, FormInputs } from "@/services/resources/forms";
import FilterForms from "@/components/Forms/Filter";
import { notifications } from "@mantine/notifications";
export function Forms() {
const [ searchParams, setSearchParams ] = useSearchParams();
@@ -52,11 +53,16 @@ export function Forms() {
await createFormMutation.mutateAsync({
...form,
start: form?.start,
end: form?.start,
end: form?.end,
productor_id: Number(form.productor_id),
referer_id: Number(form.referer_id)
referer_id: Number(form.referer_id),
minimum_shipment_value: Number(form.minimum_shipment_value),
});
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully created form", {capfirst: true}),
});
}, [createFormMutation]);
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
@@ -67,12 +73,17 @@ export function Forms() {
form: {
...form,
start: form.start,
end: form.start,
end: form.end,
productor_id: Number(form.productor_id),
referer_id: Number(form.referer_id)
referer_id: Number(form.referer_id),
minimum_shipment_value: Number(form.minimum_shipment_value),
}
});
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully edited form", {capfirst: true}),
});
}, [editFormMutation]);
const onFilterChange = useCallback((

View File

@@ -1,9 +1,8 @@
import { Flex } from "@mantine/core";
import { t } from "@/config/i18n";
import { useParams } from "react-router";
import { Flex, Text } from "@mantine/core";
import { getForms } from "@/services/api";
import { FormCard } from "@/components/Forms/Card";
import type { Form } from "@/services/resources/forms";
import { t } from "@/config/i18n";
export function Home() {
const { data: allForms } = getForms();
@@ -11,9 +10,11 @@ export function Home() {
return (
<Flex gap="md" wrap="wrap" justify="center">
{
allForms?.map((form: Form) => (
<FormCard form={form} key={form.id}/>
))
allForms && allForms?.length > 0 ?
allForms.map((form: Form) => (
<FormCard form={form} key={form.id}/>
)) :
<Text mt="lg" size="lg">{t("there is no contract for now",{capfirst: true})}</Text>
}
</Flex>
);

View File

@@ -0,0 +1,24 @@
import { t } from "@/config/i18n";
import { ActionIcon, Stack, Text, Title, Tooltip } from "@mantine/core";
import { IconHome } from "@tabler/icons-react";
import { useNavigate } from "react-router";
export function NotFound() {
const navigate = useNavigate()
return (
<Stack justify="center" align="center">
<Title order={2}>{t("oops", {capfirst: true})}</Title>
<Text>{t('this page does not exists', {capfirst: true})}</Text>
<Tooltip label={t('back to home', {capfirst: true})}>
<ActionIcon
aria-label={t("back to home", {capfirst: true})}
onClick={() => {
navigate('/')
}}
>
<IconHome/>
</ActionIcon>
</Tooltip>
</Stack>
);
}

View File

@@ -8,6 +8,7 @@ import { ProductorModal } from "@/components/Productors/Modal";
import { useCallback, useMemo } from "react";
import type { Productor, ProductorInputs } from "@/services/resources/productors";
import ProductorsFilters from "@/components/Productors/Filter";
import { notifications } from "@mantine/notifications";
export default function Productors() {
const [ searchParams, setSearchParams ] = useSearchParams();
@@ -50,6 +51,10 @@ export default function Productors() {
...productor
});
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully created productor", {capfirst: true}),
});
}, [createProductorMutation]);
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => {
@@ -60,6 +65,10 @@ export default function Productors() {
productor: productor
});
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully edited productor", {capfirst: true}),
});
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => {
@@ -116,7 +125,7 @@ export default function Productors() {
<Table.Th>{t("name", {capfirst: true})}</Table.Th>
<Table.Th>{t("type", {capfirst: true})}</Table.Th>
<Table.Th>{t("address", {capfirst: true})}</Table.Th>
<Table.Th>{t("payment", {capfirst: true})}</Table.Th>
<Table.Th>{t("payment methods", {capfirst: true})}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
</Table.Tr>
</Table.Thead>

View File

@@ -8,6 +8,7 @@ import { ProductModal } from "@/components/Products/Modal";
import { useCallback, useMemo } from "react";
import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products";
import ProductsFilters from "@/components/Products/Filter";
import { notifications } from "@mantine/notifications";
export default function Products() {
const [ searchParams, setSearchParams ] = useSearchParams();
@@ -48,6 +49,10 @@ export default function Products() {
const handleCreateProduct = useCallback(async (product: ProductInputs) => {
await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully created product", {capfirst: true}),
});
}, [createProductMutation]);
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => {
@@ -58,6 +63,10 @@ export default function Products() {
product: productCreateFromProductInputs(product)
});
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully edited product", {capfirst: true}),
});
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => {
@@ -116,7 +125,6 @@ export default function Products() {
<Table.Th>{t("price", {capfirst: true})}</Table.Th>
<Table.Th>{t("priceKg", {capfirst: true})}</Table.Th>
<Table.Th>{t("quantity", {capfirst: true})}</Table.Th>
<Table.Th>{t("quantity unit", {capfirst: true})}</Table.Th>
<Table.Th>{t("unit", {capfirst: true})}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th>
</Table.Tr>

View File

@@ -8,6 +8,7 @@ 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";
import { notifications } from "@mantine/notifications";
export default function Shipments() {
const [ searchParams, setSearchParams ] = useSearchParams();
@@ -43,6 +44,10 @@ export default function Shipments() {
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => {
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully created shipment", {capfirst: true}),
});
}, [createShipmentMutation]);
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => {
@@ -53,6 +58,10 @@ export default function Shipments() {
shipment: shipmentCreateFromShipmentInputs(shipment)
});
closeModal();
notifications.show({
title: t("success", {capfirst: true}),
message: t("successfully edited shipment", {capfirst: true}),
});
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => {

View File

@@ -42,18 +42,18 @@ export default function Users() {
const editUserMutation = editUser();
const handleCreateUser = useCallback(async (user: UserInputs) => {
await createUserMutation.mutateAsync(user);
closeModal();
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();
await editUserMutation.mutateAsync({
id: id,
user: user
});
closeModal();
}, []);
const onFilterChange = useCallback((values: string[], filter: string) => {