add contract pdf generation
This commit is contained in:
@@ -16,6 +16,7 @@ export function FormCard({form}: FormCardProps) {
|
||||
<Box
|
||||
component={Link}
|
||||
to={`/form/${form.id}`}
|
||||
style={{textDecoration: "none", color: "black"}}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Title
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Group, Modal, Select, TextInput, type ModalBaseProps } from "@mantine/core";
|
||||
import { Button, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import { IconCancel } from "@tabler/icons-react";
|
||||
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
|
||||
import { getProductors, getUsers } from "@/services/api";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useEffect, useMemo } from "react";
|
||||
@@ -20,6 +20,7 @@ export default function FormModal({
|
||||
}: FormModalProps) {
|
||||
const {data: productors} = getProductors();
|
||||
const {data: users} = getUsers();
|
||||
|
||||
const form = useForm<FormInputs>({
|
||||
initialValues: {
|
||||
name: "",
|
||||
@@ -28,6 +29,7 @@ export default function FormModal({
|
||||
end: null,
|
||||
productor_id: "",
|
||||
referer_id: "",
|
||||
minimum_shipment_value: null,
|
||||
},
|
||||
validate: {
|
||||
name: (value) =>
|
||||
@@ -67,10 +69,9 @@ export default function FormModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={currentForm ? t("edit form") : t('create form')}
|
||||
title={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
|
||||
>
|
||||
<TextInput
|
||||
label={t("form name", {capfirst: true})}
|
||||
@@ -123,6 +124,13 @@ export default function FormModal({
|
||||
data={productorsSelect || []}
|
||||
{...form.getInputProps('productor_id')}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("minimum shipment value", {capfirst: true})}
|
||||
placeholder={t("minimum shipment value", {capfirst: true})}
|
||||
description={t("some contracts require a minimum value per shipment, ignore this field if it's not the case", {capfirst: true})}
|
||||
radius="sm"
|
||||
{...form.getInputProps('minimum_shipment_value')}
|
||||
/>
|
||||
<Group mt="sm" justify="space-between">
|
||||
<Button
|
||||
variant="filled"
|
||||
@@ -137,6 +145,7 @@ export default function FormModal({
|
||||
<Button
|
||||
variant="filled"
|
||||
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
|
||||
leftSection={currentForm ? <IconEdit/> : <IconPlus/>}
|
||||
onClick={() => {
|
||||
form.validate();
|
||||
if (form.isValid()) {
|
||||
|
||||
28
frontend/src/components/Label/index.tsx
Normal file
28
frontend/src/components/Label/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
export type InputLabelProps = {
|
||||
label: string;
|
||||
info: string;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
export function InputLabel({label, info, isRequired}: InputLabelProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Tooltip label={info}>
|
||||
<ActionIcon variant="transparent" size="xs" color="gray">
|
||||
<IconInfoCircle size={16}/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<span>
|
||||
{label}
|
||||
{
|
||||
isRequired ?
|
||||
<span style={{ color: 'red' }}> *</span> : null
|
||||
}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-self: left;
|
||||
width: 50%;
|
||||
padding: 1rem;
|
||||
background-color: var(--mantine-color-blue-4);
|
||||
}
|
||||
|
||||
a {
|
||||
gap: 1em;
|
||||
.navLink {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
margin-right: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,8 +5,18 @@ import "./index.css";
|
||||
export function Navbar() {
|
||||
return (
|
||||
<nav>
|
||||
<NavLink to="/">{t("home", {capfirst: true})}</NavLink>
|
||||
<NavLink to="/dashboard/productors">{t("dashboard", {capfirst: true})}</NavLink>
|
||||
<NavLink
|
||||
className={"navLink"}
|
||||
to="/"
|
||||
>
|
||||
{t("home", {capfirst: true})}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={"navLink"}
|
||||
to="/dashboard/productors"
|
||||
>
|
||||
{t("dashboard", {capfirst: true})}
|
||||
</NavLink>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
|
||||
import { Button, Group, Modal, MultiSelect, 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 { Productor, ProductorInputs } from "@/services/resources/productors";
|
||||
import { PaymentMethods, type Productor, type ProductorInputs } from "@/services/resources/productors";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export type ProductorModalProps = ModalBaseProps & {
|
||||
@@ -20,18 +20,16 @@ export function ProductorModal({
|
||||
initialValues: {
|
||||
name: "",
|
||||
address: "",
|
||||
payment: "",
|
||||
payment_methods: [],
|
||||
type: "",
|
||||
},
|
||||
validate: {
|
||||
name: (value) =>
|
||||
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
|
||||
!value ? `${t("name", {capfirst: true})} ${t("is required")}` : null,
|
||||
address: (value) =>
|
||||
!value ? `${t("address", {capfirst: true})} ${t('is required')}` : null,
|
||||
payment: (value) =>
|
||||
!value ? `${t("payment", {capfirst: true})} ${t('is required')}` : null,
|
||||
!value ? `${t("address", {capfirst: true})} ${t("is required")}` : null,
|
||||
type: (value) =>
|
||||
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null
|
||||
!value ? `${t("type", {capfirst: true})} ${t("is required")}` : null
|
||||
}
|
||||
});
|
||||
|
||||
@@ -45,7 +43,6 @@ export function ProductorModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("create productor", {capfirst: true})}
|
||||
@@ -56,30 +53,65 @@ export function ProductorModal({
|
||||
placeholder={t("productor name", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("productor type", {capfirst: true})}
|
||||
placeholder={t("productor type", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('type')}
|
||||
{...form.getInputProps("type")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("productor address", {capfirst: true})}
|
||||
placeholder={t("productor address", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('address')}
|
||||
{...form.getInputProps("address")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("productor payment", {capfirst: true})}
|
||||
placeholder={t("productor payment", {capfirst: true})}
|
||||
<MultiSelect
|
||||
label={t("payment methods", {capfirst: true})}
|
||||
placeholder={t("payment methods", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('payment')}
|
||||
data={PaymentMethods}
|
||||
clearable
|
||||
searchable
|
||||
value={form.values.payment_methods.map(p => p.name)}
|
||||
onChange={(names) => {
|
||||
form.setFieldValue("payment_methods", names.map(name => {
|
||||
const existing = form.values.payment_methods.find(p => p.name === name);
|
||||
return existing ?? {
|
||||
name,
|
||||
details: ""
|
||||
};
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
|
||||
{
|
||||
form.values.payment_methods.map((method, index) => (
|
||||
<TextInput
|
||||
key={index}
|
||||
label={
|
||||
method.name === "cheque" ?
|
||||
t("order name", {capfirst: true}) :
|
||||
method.name === "transfer" ?
|
||||
t("IBAN") :
|
||||
t("details", {capfirst: true})
|
||||
}
|
||||
placeholder={
|
||||
method.name === "cheque" ?
|
||||
t("order name", {capfirst: true}) :
|
||||
method.name === "transfer" ?
|
||||
t("IBAN") :
|
||||
t("details", {capfirst: true})
|
||||
}
|
||||
{...form.getInputProps(
|
||||
`payment_methods.${index}.details`
|
||||
)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<Group mt="sm" justify="space-between">
|
||||
<Button
|
||||
variant="filled"
|
||||
@@ -93,14 +125,15 @@ export function ProductorModal({
|
||||
>{t("cancel", {capfirst: true})}</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
aria-label={currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}
|
||||
aria-label={currentProductor ? t("edit productor", {capfirst: true}) : t("create productor", {capfirst: true})}
|
||||
onClick={() => {
|
||||
form.validate();
|
||||
console.log(form.getValues())
|
||||
if (form.isValid()) {
|
||||
handleSubmit(form.getValues(), currentProductor?.id)
|
||||
}
|
||||
}}
|
||||
>{currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}</Button>
|
||||
>{currentProductor ? t("edit productor", {capfirst: true}) : t("create productor", {capfirst: true})}</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionIcon, Table, Tooltip } from "@mantine/core";
|
||||
import { ActionIcon, Badge, Table, Tooltip } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { IconEdit, IconX } from "@tabler/icons-react";
|
||||
import type { Productor } from "@/services/resources/productors";
|
||||
@@ -21,7 +21,15 @@ export default function ProductorRow({
|
||||
<Table.Td>{productor.name}</Table.Td>
|
||||
<Table.Td>{productor.type}</Table.Td>
|
||||
<Table.Td>{productor.address}</Table.Td>
|
||||
<Table.Td>{productor.payment}</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
productor.payment_methods.map((value) =>(
|
||||
<Badge ml="xs">
|
||||
{t(value.name, {capfirst: true})}
|
||||
</Badge>
|
||||
))
|
||||
}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label={t("edit productor", {capfirst: true})}>
|
||||
<ActionIcon
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Button, Group, Modal, NumberInput, Pill, Select, TextInput, Title, Tooltip, type ModalBaseProps } from "@mantine/core";
|
||||
import { Button, Group, Modal, NumberInput, Select, TextInput, Title, type ModalBaseProps } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { IconCancel, IconInfoCircle } from "@tabler/icons-react";
|
||||
import { IconCancel } from "@tabler/icons-react";
|
||||
import { ProductQuantityUnit, productToProductInputs, ProductUnit, type Product, type ProductInputs } from "@/services/resources/products";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { getProductors } from "@/services/api";
|
||||
import { InputLabel } from "@/components/Label";
|
||||
|
||||
export type ProductModalProps = ModalBaseProps & {
|
||||
currentProduct?: Product;
|
||||
@@ -42,7 +43,7 @@ export function ProductModal({
|
||||
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null,
|
||||
productor_id: (value) =>
|
||||
!value ? `${t("productor", {capfirst: true})} ${t('is required')}` : null
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,7 +58,6 @@ export function ProductModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("create product", {capfirst: true})}
|
||||
@@ -78,19 +78,23 @@ export function ProductModal({
|
||||
label={t("product name", {capfirst: true})}
|
||||
placeholder={t("product name", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Select
|
||||
label={t("product type", {capfirst: true})}
|
||||
label={
|
||||
<InputLabel
|
||||
label={t("product type", {capfirst: true})}
|
||||
info={t("recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)", {capfirst: true})}
|
||||
isRequired
|
||||
/>
|
||||
}
|
||||
placeholder={t("product type", {capfirst: true})}
|
||||
radius="sm"
|
||||
withAsterisk
|
||||
searchable
|
||||
clearable
|
||||
data={[
|
||||
{value: "1", label: t("planned")},
|
||||
{value: "2", label: t("recurrent")}
|
||||
{value: "1", label: t("planned", {capfirst: true})},
|
||||
{value: "2", label: t("recurrent", {capfirst: true})}
|
||||
]}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
@@ -103,7 +107,7 @@ export function ProductModal({
|
||||
withAsterisk
|
||||
searchable
|
||||
clearable
|
||||
data={Object.entries(ProductUnit).map(([key, value]) => ({value: key, label: t(value)}))}
|
||||
data={Object.entries(ProductUnit).map(([key, value]) => ({value: key, label: t(value, {capfirst: true})}))}
|
||||
{...form.getInputProps('unit')}
|
||||
/>
|
||||
<Group grow>
|
||||
@@ -131,7 +135,9 @@ export function ProductModal({
|
||||
label={t("product quantity unit", {capfirst: true})}
|
||||
placeholder={t("product quantity unit", {capfirst: true})}
|
||||
radius="sm"
|
||||
data={Object.entries(ProductQuantityUnit).map(([key, value]) => ({value: key, label: t(value)}))}
|
||||
clearable
|
||||
searchable
|
||||
data={Object.entries(ProductQuantityUnit).map(([key, value]) => ({value: key, label: t(value, {capfirst: true})}))}
|
||||
{...form.getInputProps('quantity_unit', {capfirst: true})}
|
||||
/>
|
||||
|
||||
@@ -152,7 +158,6 @@ export function ProductModal({
|
||||
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
|
||||
onClick={() => {
|
||||
form.validate();
|
||||
console.log(form.isValid(), form.getValues())
|
||||
if (form.isValid()) {
|
||||
handleSubmit(form.getValues(), currentProduct?.id)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,27 @@ export default function ProductRow({
|
||||
<Table.Tr key={product.id}>
|
||||
<Table.Td>{product.name}</Table.Td>
|
||||
<Table.Td>{t(ProductType[product.type])}</Table.Td>
|
||||
<Table.Td>{product.price}</Table.Td>
|
||||
<Table.Td>{product.price_kg}</Table.Td>
|
||||
<Table.Td>{product.quantity}</Table.Td>
|
||||
<Table.Td>{product.quantity_unit}</Table.Td>
|
||||
<Table.Td>{t(ProductUnit[product.unit])}</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
product.price ?
|
||||
Intl.NumberFormat(
|
||||
"fr-FR",
|
||||
{style: "currency", currency: "EUR"}
|
||||
).format(product.price) : null
|
||||
}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
product.price_kg ?
|
||||
`${Intl.NumberFormat(
|
||||
"fr-FR",
|
||||
{style: "currency", currency: "EUR"}
|
||||
).format(product.price_kg)}/kg` : null
|
||||
|
||||
}
|
||||
</Table.Td>
|
||||
<Table.Td>{product.quantity}{product.quantity_unit}</Table.Td>
|
||||
<Table.Td>{t(ProductUnit[product.unit], {capfirst: true})}</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label={t("edit product", {capfirst: true})}>
|
||||
<ActionIcon
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Accordion, Group, Text } from "@mantine/core";
|
||||
import { Accordion, Group, Stack, Text } from "@mantine/core";
|
||||
import type { Shipment } from "@/services/resources/shipments";
|
||||
import { ProductForm } from "@/components/Products/Form";
|
||||
import type { UseFormReturnType } from "@mantine/form";
|
||||
import { useMemo } from "react";
|
||||
import { computePrices } from "@/pages/Contract";
|
||||
import { t } from "@/config/i18n";
|
||||
|
||||
export type ShipmentFormProps = {
|
||||
inputForm: UseFormReturnType<Record<string, string | number>>;
|
||||
shipment: Shipment;
|
||||
minimumPrice?: number | null;
|
||||
index: number;
|
||||
}
|
||||
|
||||
@@ -15,6 +17,7 @@ export default function ShipmentForm({
|
||||
shipment,
|
||||
index,
|
||||
inputForm,
|
||||
minimumPrice,
|
||||
}: ShipmentFormProps) {
|
||||
const shipmentPrice = useMemo(() => {
|
||||
const values = Object
|
||||
@@ -26,18 +29,42 @@ export default function ShipmentForm({
|
||||
return computePrices(values, shipment.products);
|
||||
}, [inputForm, shipment.products]);
|
||||
|
||||
const priceRequirement = useMemo(() => {
|
||||
if (!minimumPrice)
|
||||
return false;
|
||||
return minimumPrice ? shipmentPrice < minimumPrice : true
|
||||
}, [shipmentPrice, minimumPrice])
|
||||
|
||||
return (
|
||||
<Accordion.Item value={String(index)}>
|
||||
<Accordion.Control>
|
||||
<Group justify="space-between">
|
||||
<Text>{shipment.name}</Text>
|
||||
<Text>{
|
||||
<Text>{shipment.name}</Text>
|
||||
<Stack gap={0}>
|
||||
<Text c={priceRequirement ? "red" : "green"}>{
|
||||
Intl.NumberFormat(
|
||||
"fr-FR",
|
||||
{style: "currency", currency: "EUR"}
|
||||
).format(shipmentPrice)
|
||||
}</Text>
|
||||
<Text mr="lg">{shipment.date}</Text>
|
||||
{
|
||||
priceRequirement ?
|
||||
<Text c="red"size="sm">
|
||||
{`${t("minimum price for this shipment should be at least", {capfirst: true})} ${minimumPrice}€`}
|
||||
</Text> :
|
||||
null
|
||||
}
|
||||
</Stack>
|
||||
<Text mr="lg">
|
||||
{`${
|
||||
new Date(shipment.date).toLocaleDateString("fr-FR", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
}`}
|
||||
</Text>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
|
||||
@@ -63,7 +63,6 @@ export default function ShipmentModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={currentShipment ? t("edit shipment") : t('create shipment')}
|
||||
@@ -93,6 +92,7 @@ export default function ShipmentModal({
|
||||
<MultiSelect
|
||||
label={t("shipment products", {capfirst: true})}
|
||||
placeholder={t("shipment products", {capfirst: true})}
|
||||
description={t("shipment products is necessary only for planned products (if all products are recurrent leave empty)", {capfirst: true})}
|
||||
data={productsSelect || []}
|
||||
clearable
|
||||
searchable
|
||||
|
||||
@@ -37,7 +37,6 @@ export function UserModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
w={{base: "100%", md: "80%", lg: "50%"}}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("create user", {capfirst: true})}
|
||||
|
||||
Reference in New Issue
Block a user