add contract storage fix various bugs and translations

This commit is contained in:
2026-02-16 01:23:31 +01:00
parent 627ddfc464
commit be8e32ebed
28 changed files with 1225 additions and 401 deletions

View File

@@ -0,0 +1,30 @@
import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react";
import { t } from "@/config/i18n";
export type ContractFiltersProps = {
forms: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
};
export default function ContractFilters({ forms, filters, onFilterChange }: ContractFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("forms");
}, [filters]);
return (
<Group>
<MultiSelect
aria-label={t("filter by form", { capfirst: true })}
placeholder={t("filter by form", { capfirst: true })}
data={forms}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, "forms");
}}
clearable
/>
</Group>
);
}

View File

@@ -0,0 +1,84 @@
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
import { type Contract, type ContractInputs } from "@/services/resources/contracts";
export type ContractModalProps = ModalBaseProps & {
currentContract?: Contract;
handleSubmit: (contract: ContractInputs, id?: number) => void;
};
export function ContractModal({
opened,
onClose,
currentContract,
handleSubmit,
}: ContractModalProps) {
const form = useForm<ContractInputs>({
// initialValues: {
// firstname: currentContract?.firstname ?? "",
// lastname: currentContract?.lastname ?? "",
// email: currentContract?.email ?? "",
// },
// validate: {
// firstname: (value) =>
// !value ? `${t("name", { capfirst: true })} ${t("is required")}` : null,
// email: (value) =>
// !value ? `${t("email", { capfirst: true })} ${t("is required")}` : null,
// },
});
return (
<Modal opened={opened} onClose={onClose} title={t("create contract", { capfirst: true })}>
<Title order={4}>{t("informations", { capfirst: true })}</Title>
<TextInput
label={t("contract name", { capfirst: true })}
placeholder={t("contract name", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
label={t("contract email", { capfirst: true })}
placeholder={t("contract email", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps("email")}
/>
<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={
currentContract
? t("edit contract", { capfirst: true })
: t("create contract", { capfirst: true })
}
leftSection={currentContract ? <IconEdit /> : <IconPlus />}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentContract?.id);
}
}}
>
{currentContract
? t("edit contract", { capfirst: true })
: t("create contract", { capfirst: true })}
</Button>
</Group>
</Modal>
);
}

View File

@@ -0,0 +1,55 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { type Contract } from "@/services/resources/contracts";
import { IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n";
import { useDeleteContract } from "@/services/api";
export type ContractRowProps = {
contract: Contract;
};
export default function ContractRow({ contract }: ContractRowProps) {
// const [searchParams] = useSearchParams();
const deleteMutation = useDeleteContract();
// const navigate = useNavigate();
return (
<Table.Tr key={contract.id}>
<Table.Td>
{contract.firstname} {contract.lastname}
</Table.Td>
<Table.Td>{contract.email}</Table.Td>
<Table.Td>
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
</Table.Td>
<Table.Td>
{/* <Tooltip label={t("edit contract", { capfirst: true })}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(
`/dashboard/contracts/${contract.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconEdit />
</ActionIcon>
</Tooltip> */}
<Tooltip label={t("remove contract", { capfirst: true })}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(contract.id);
}}
>
<IconX />
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -0,0 +1,78 @@
import { t } from "@/config/i18n";
import type { ContractInputs } from "@/services/resources/contracts";
import { Group, NumberInput, Stack, TextInput, Title } from "@mantine/core";
import type { UseFormReturnType } from "@mantine/form";
import { useEffect } from "react";
export type ContractChequeProps = {
inputForm: UseFormReturnType<ContractInputs>;
price: number;
chequeOrder: string;
};
export type Cheque = {
name: string;
value: string;
};
export function ContractCheque({ inputForm, price, chequeOrder }: ContractChequeProps) {
useEffect(() => {
if (!inputForm.values.payment_method.includes("cheque")) {
return;
}
const quantity = Number(inputForm.values.cheque_quantity);
if (!quantity || quantity <= 0) return;
const cheques = inputForm.values.cheques || [];
if (cheques.length !== quantity) {
const newCheques = Array.from({ length: quantity }, (_, i) => ({
name: cheques[i]?.name ?? "",
value: cheques[i]?.value ?? 0,
}));
inputForm.setFieldValue("cheques", newCheques);
}
const totalCents = Math.round(price * 100);
const base = Math.floor(totalCents / quantity);
const rest = totalCents - base * quantity;
for (let i = 0; i < quantity; i++) {
const val = (i === quantity - 1 ? base + rest : base) / 100;
inputForm.setFieldValue(`cheques.${i}.value`, val.toFixed(2));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputForm.values.cheque_quantity, price, inputForm.values.cheques]);
return (
<Stack>
<Title order={4}>{`${t("order name")} : ${chequeOrder}`}</Title>
<NumberInput
label={t("cheque quantity", { capfirst: true })}
placeholder={t("enter cheque quantity", { capfirst: true })}
description={t(
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically",
{ capfirst: true },
)}
min={1}
max={3}
{...inputForm.getInputProps(`cheque_quantity`)}
/>
<Group grow>
{inputForm.values.cheques.map((_cheque, index) => (
<Stack key={`${index}`}>
<TextInput
label={t("cheque id", { capfirst: true })}
placeholder={t("cheque id", { capfirst: true })}
{...inputForm.getInputProps(`cheques.${index}.name`)}
/>
<NumberInput
readOnly
label={t("cheque value", { capfirst: true })}
suffix={"€"}
placeholder={t("enter cheque value", { capfirst: true })}
{...inputForm.getInputProps(`cheques.${index}.value`)}
/>
</Stack>
))}
</Group>
</Stack>
);
}

View File

@@ -9,7 +9,7 @@ import {
} from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react";
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
import {
PaymentMethods,
type Productor,
@@ -94,26 +94,16 @@ export function ProductorModal({
);
}}
/>
{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`)}
/>
))}
{form.values.payment_methods.map((method, index) =>
method.name === "cheque" ? (
<TextInput
key={index}
label={t("order name", { capfirst: true })}
placeholder={t("order name", { capfirst: true })}
{...form.getInputProps(`payment_methods.${index}.details`)}
/>
) : null,
)}
<Group mt="sm" justify="space-between">
<Button
variant="filled"
@@ -134,6 +124,7 @@ export function ProductorModal({
? t("edit productor", { capfirst: true })
: t("create productor", { capfirst: true })
}
leftSection={currentProductor ? <IconEdit /> : <IconPlus />}
onClick={() => {
form.validate();
if (form.isValid()) {

View File

@@ -1,11 +1,12 @@
import { t } from "@/config/i18n";
import type { ContractInputs } from "@/services/resources/contracts";
import { ProductUnit, type Product } from "@/services/resources/products";
import type { Shipment } from "@/services/resources/shipments";
import { Group, NumberInput } from "@mantine/core";
import type { UseFormReturnType } from "@mantine/form";
export type ProductFormProps = {
inputForm: UseFormReturnType<Record<string, string | number>>;
inputForm: UseFormReturnType<ContractInputs>;
product: Product;
shipment?: Shipment;
};
@@ -33,7 +34,9 @@ export function ProductForm({ inputForm, product, shipment }: ProductFormProps)
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t(ProductUnit[product.unit])}`}
{...inputForm.getInputProps(
shipment ? `planned-${shipment.id}-${product.id}` : `recurrent-${product.id}`,
shipment
? `products.planned-${shipment.id}-${product.id}`
: `products.recurrent-${product.id}`,
)}
/>
</Group>

View File

@@ -10,7 +10,7 @@ import {
} from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react";
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
import {
ProductQuantityUnit,
ProductUnit,
@@ -179,6 +179,7 @@ export function ProductModal({ opened, onClose, currentProduct, handleSubmit }:
? t("edit product", { capfirst: true })
: t("create product", { capfirst: true })
}
leftSection={currentProduct ? <IconEdit /> : <IconPlus />}
onClick={() => {
form.validate();
if (form.isValid()) {

View File

@@ -5,9 +5,10 @@ import type { UseFormReturnType } from "@mantine/form";
import { useMemo } from "react";
import { t } from "@/config/i18n";
import { computePrices } from "@/pages/Contract/price";
import type { ContractInputs } from "@/services/resources/contracts";
export type ShipmentFormProps = {
inputForm: UseFormReturnType<Record<string, string | number>>;
inputForm: UseFormReturnType<ContractInputs>;
shipment: Shipment;
minimumPrice?: number | null;
index: number;
@@ -20,7 +21,7 @@ export default function ShipmentForm({
minimumPrice,
}: ShipmentFormProps) {
const shipmentPrice = useMemo(() => {
const values = Object.entries(inputForm.getValues()).filter(
const values = Object.entries(inputForm.getValues().products).filter(
([key]) => key.includes("planned") && key.split("-")[1] === String(shipment.id),
);
return computePrices(values, shipment.products);

View File

@@ -9,7 +9,7 @@ import {
} 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 { useForm } from "@mantine/form";
import { useMemo } from "react";
import { type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
@@ -129,6 +129,7 @@ export default function ShipmentModal({
? t("edit shipment", { capfirst: true })
: t("create shipment", { capfirst: true })
}
leftSection={currentShipment ? <IconEdit /> : <IconPlus />}
onClick={() => {
form.validate();
if (form.isValid()) {

View File

@@ -1,7 +1,7 @@
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 { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
import { type User, type UserInputs } from "@/services/resources/users";
export type UserModalProps = ModalBaseProps & {
@@ -60,6 +60,7 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
? t("edit user", { capfirst: true })
: t("create user", { capfirst: true })
}
leftSection={currentUser ? <IconEdit /> : <IconPlus />}
onClick={() => {
form.validate();
if (form.isValid()) {