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

@@ -5,4 +5,5 @@ coverage
.next
out
public
*.lock
*.lock
*.html

View File

@@ -1,19 +1,19 @@
{
"product name": "product name",
"product price": "product price",
"product quantity": "product quantity",
"product quantity unit": "product quantity unit",
"product type": "product type",
"product name": "name of the product",
"product price": "price of the product",
"product quantity": "quantity of the product",
"product quantity unit": "unit of the product quantity",
"product type": "type of the product",
"planned": "planned",
"planned products": "planned products",
"select products per shipment": "select products per shipment.",
"select products per shipment": "select products for each shipment",
"recurrent products": "recurrent products",
"your selection in this category will apply for all shipments": "your selection in this category will apply for all shipments.",
"your selection in this category will apply for all shipments": "selection in this category applies to all shipments",
"recurrent": "recurrent",
"product price kg": "product price kg",
"product price kg": "price per kilogram",
"product unit": "product unit",
"grams": "grams",
"kilo": "kilo",
"kilo": "kilogram",
"piece": "piece",
"filter by season": "filter by season",
"filter by form": "filter by form",
@@ -25,13 +25,13 @@
"productor": "productor",
"referer": "referer",
"edit form": "edit form",
"form name": "form name",
"form name": "name of the form",
"contract season": "contract season",
"contract season recommandation": "recommandation : <Season>-<year> (example: Winter-2025)",
"contract season recommandation": "recommendation: <Season>-<year> (example: Winter-2025)",
"start date": "start date",
"end date": "end date",
"nothing found": "nothing found",
"number of shipment": "number of shipment",
"number of shipment": "number of shipments",
"cancel": "cancel",
"create form": "create form",
"edit productor": "edit productor",
@@ -46,32 +46,32 @@
"transfer": "transfer",
"type": "type",
"create productor": "create productor",
"productor name": "productor name",
"productor type": "productor type",
"productor address": "productor address",
"productor payment": "productor payment",
"priceKg": "priceKg",
"productor name": "name of the productor",
"productor type": "type of the productor",
"productor address": "address of the productor",
"productor payment": "payment method of the productor",
"priceKg": "price per kilogram",
"quantity": "quantity",
"quantity unit": "quantity unit",
"quantity unit": "unit of quantity",
"unit": "sell unit",
"price": "price",
"create product": "create product",
"informations": "informations",
"informations": "information",
"remove product": "remove product",
"edit product": "edit product",
"shipment name": "shipment name",
"shipments": "shipments",
"shipment": "shipment",
"there is": "there is",
"for this contract": "for this contact.",
"for this contract": "for this contract",
"shipment date": "shipment date",
"shipment products": "shipment products",
"minimum shipment value": "minimum shipment value",
"shipment form": "shipment form",
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "shipment products is necessary only for planned products (if all products are recurrent leave empty)",
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "recurrent product is for all shipments, planned product is for a specific shipment (see shipment form).",
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "some contracts require a minimum value per shipment, ignore this field if it's not the case.",
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "shipment products required only for planned products (leave empty if all products are recurrent)",
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "recurrent products apply to all shipments, planned products apply to a specific shipment (see shipment form)",
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "ignore this field if minimum shipment value is not required",
"minimum price for this shipment should be at least": "minimum price for this shipment",
"remove shipment": "remove shipment",
"productors": "productors",
"products": "products",
@@ -83,6 +83,8 @@
"actions": "actions",
"all productors": "all productors",
"all products": "all products",
"all shipments": "all shipments",
"all referers": "all referers",
"a name": "a name",
"a season": "a season",
"a start date": "a start date",
@@ -90,21 +92,22 @@
"a productor": "a productor",
"a referer": "a referer",
"a phone": "a phone",
"a fistname": "a fistname",
"a fistname": "a firstname",
"a lastname": "a lastname",
"a email": "a email",
"a email": "an email",
"a payment method": "a payment method",
"submit contract": "submit contract",
"success": "success",
"successfully edited user": "successfully edited user",
"successfully edited form": "successfully edited form",
"successfully edited product": "successfully edited product",
"successfully edited productor": "successfully edited productor",
"successfully edited shipment": "successfully edited shipment",
"successfully created user": "successfully created user",
"successfully created form": "successfully created form",
"successfully created product": "successfully created product",
"successfully created productor": "successfully created productor",
"successfully created shipment": "successfully created shipment",
"successfully edited user": "user edited successfully",
"successfully edited form": "form edited successfully",
"successfully edited product": "product edited successfully",
"successfully edited productor": "productor edited successfully",
"successfully edited shipment": "shipment edited successfully",
"successfully created user": "user created successfully",
"successfully created form": "form created successfully",
"successfully created product": "product created successfully",
"successfully created productor": "productor created successfully",
"successfully created shipment": "shipment created successfully",
"error": "error",
"error editing user": "error editing user",
"error editing form": "error editing form",
@@ -121,7 +124,17 @@
"error deleting product": "error deleting product",
"error deleting productor": "error deleting productor",
"error deleting shipment": "error deleting shipment",
"there is no contract for now": "there is no contract for now.",
"the product unit will be assigned to the quantity requested in the form": "the product unit will be assigned to the quantity requested in the form",
"all theses informations are for contract generation": "all theses informations are for contract generation."
"there is no contract for now": "no contracts available currently",
"for transfer method contact your referer or productor": "for transfer method, contact your referer or productor",
"cheque quantity": "number of cheques",
"enter cheque quantity": "enter number of cheques",
"cheque id": "cheque identifier",
"cheque value": "cheque value",
"enter cheque value": "enter cheque value",
"payment method": "payment method",
"enter payment method": "enter payment method",
"choose payment method": "choose a payment method (no actual payments, information only)",
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically": "enter 1 to 3 cheques and their identifiers; values are calculated automatically",
"the product unit will be assigned to the quantity requested in the form": "product unit matches quantity requested in the form",
"all theses informations are for contract generation": "all this information is used for contract generation"
}

View File

@@ -6,15 +6,15 @@
"product type": "type de produit",
"planned": "planifié",
"planned products": "Produits planifiés par livraison",
"select products per shipment": "Selectionnez les produits pour chaque livraison.",
"select products per shipment": "Sélectionnez les produits pour chaque livraison.",
"recurrent": "récurent",
"recurrent products": "Produits récurents",
"your selection in this category will apply for all shipments": "votre selection sera appliquée pour chaque livraisons (Exemple: Pour 6 livraisons, le produits sera comptés 6 fois : une fois par livraison).",
"recurrent products": "Produits récurrents",
"your selection in this category will apply for all shipments": "votre sélection sera appliquée pour chaque livraisons (Exemple: Pour 6 livraisons, le produits sera comptés 6 fois : une fois par livraison).",
"product price kg": "prix du produit au Kilo",
"product unit": "unité de vente du produit",
"piece": "pièce",
"in": "en",
"enter quantity": "entrez la quantitée",
"enter quantity": "entrez la quantité",
"filter by season": "filtrer par saisons",
"filter by form": "filtrer par formulaire",
"filter by productor": "filtrer par producteur·trice",
@@ -33,7 +33,7 @@
"nothing found": "rien à afficher",
"number of shipment": "nombre de livraisons",
"cancel": "annuler",
"create form": "créer un formulare de contrat",
"create form": "créer un formulaire de contrat",
"create productor": "créer le/la producteur·trice",
"edit productor": "modifier le/la producteur·trice",
"remove productor": "supprimer le/la producteur·trice",
@@ -68,8 +68,8 @@
"shipment products": "produits pour la livraison",
"shipment form": "formulaire lié a la livraison",
"minimum shipment value": "valeur minimum d'une livraison (€)",
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "il est nécéssaire de configurer les produits pour la livraison uniquement si il y a des produits planifiés (laisser vide si tous les produits sont récurents).",
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "les produits récurents sont pour toutes les livraisons, les produits planifiés sont pour une livraison particulière (voir formulaire de création de livraison).",
"shipment products is necessary only for planned products (if all products are recurrent leave empty)": "il est nécessaire de configurer les produits pour la livraison uniquement si il y a des produits planifiés (laisser vide si tous les produits sont récurents).",
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)": "les produits récurrents sont pour toutes les livraisons, les produits planifiés sont pour une livraison particulière (voir formulaire de création de livraison).",
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "certains contrats nécessitent une valeur minimum par livraison. Ce champ peut être ignoré sil ne sapplique pas à votre contrat.",
"minimum price for this shipment should be at least": "le prix minimum d'une livraison doit être au moins de",
"there is": "il y a",
@@ -85,6 +85,8 @@
"actions": "actions",
"all productors": "tous les producteur·trices",
"all products": "tous les produits",
"all shipments": "toutes les livraisons",
"all referers": "tous les référent·es",
"is required": "est requis·e",
"a name": "un nom",
"a season": "une saison",
@@ -96,6 +98,7 @@
"a fistname": "un prénom",
"a lastname": "un nom",
"a email": "une adresse email",
"a payment method": "une méthode de paiement",
"submit contract": "Envoyer le contrat",
"mililiter": "mililitres (ml)",
"grams": "grammes (g)",
@@ -106,17 +109,17 @@
"successfully edited form": "formulaire correctement édité",
"successfully edited product": "produit correctement édité",
"successfully edited productor": "producteur·trice correctement édité(e)",
"successfully edited shipment": "livaison correctement éditée",
"successfully edited shipment": "livraison correctement éditée",
"successfully created user": "utilisateur·trice correctement créé(e)",
"successfully created form": "formulaire correctement créé",
"successfully created product": "produit correctement créé",
"successfully created productor": "producteur·trice correctement créé(e)",
"successfully created shipment": "livaison correctement créée",
"successfully created shipment": "livraison correctement créée",
"successfully deleted user": "utilisateur·trice correctement supprimé",
"successfully deleted form": "formulaire correctement supprimé",
"successfully deleted product": "produit correctement supprimé",
"successfully deleted productor": "producteur·trice correctement supprimé(e)",
"successfully deleted shipment": "livaison correctement supprimée",
"successfully deleted shipment": "livraison correctement supprimée",
"error": "erreur",
"error editing user": "erreur pendant l'édition de l'utilisateur·trice",
"error editing form": "erreur pendant l'édition du formulaire",
@@ -134,6 +137,16 @@
"error deleting productor": "erreur pendant la suppression du producteur·trice",
"error deleting shipment": "erreur pendant la suppression de la livraison",
"there is no contract for now": "Il n'y a pas de contrats pour le moment.",
"the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée a la quantité demandée dans le formulaire des amapiens.",
"all theses informations are for contract generation": "ces informations sont nécéssaires pour la génération de contrat."
"for transfer method contact your referer or productor": "pour mettre en place le virement automatique, contactez votre référent ou le producteur.",
"cheque quantity": "quantité de chèques (pour le paiement en plusieurs fois)",
"enter cheque quantity": "Entrez la quantité de chèques",
"cheque id": "identifiant du chèque",
"cheque value": "valeur du chèque",
"enter cheque value": "entrez la valeur du chèque",
"enter payment method": "sélectionnez votre méthode de paiement",
"number of cheques between 1 and 3 cheques also enter your cheques identifiers, value is calculated automatically": "nombre de chèques entre 1 et 3, entrez également les identifiants des chèques utilisés.",
"payment method": "méthode de paiement",
"choose payment method": "choisissez votre méthode de paiement (vous n'avez pas à payer tout de suite, uniquement renseigner comment vous souhaitez régler votre commande).",
"the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée à la quantité demandée dans le formulaire des amapiens.",
"all theses informations are for contract generation": "ces informations sont nécessaires pour la génération de contrat."
}

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()) {

View File

@@ -4,10 +4,10 @@ import { RouterProvider } from "react-router";
import { router } from "@/router.tsx";
import { MantineProvider } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Notifications } from "@mantine/notifications";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/notifications/styles.css";
import { Notifications } from "@mantine/notifications";
const queryClient = new QueryClient();

View File

@@ -10,6 +10,8 @@ import {
List,
Loader,
Overlay,
Select,
Space,
Stack,
Text,
TextInput,
@@ -20,16 +22,22 @@ import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
import { useCallback, useMemo, useRef } from "react";
import { useParams } from "react-router";
import { computePrices } from "./price";
import { ContractCheque } from "@/components/PaymentMethods/Cheque";
import { tranformProducts, type ContractInputs } from "@/services/resources/contracts";
export function Contract() {
const { id } = useParams();
const { data: form } = useGetForm(Number(id), { enabled: !!id });
const inputForm = useForm<Record<string, number | string>>({
const inputForm = useForm<ContractInputs>({
initialValues: {
firstname: "",
lastname: "",
email: "",
phone: "",
payment_method: "",
cheque_quantity: 1,
cheques: [],
products: {},
},
validate: {
firstname: (value) =>
@@ -40,6 +48,8 @@ export function Contract() {
!value ? `${t("a email", { capfirst: true })} ${t("is required")}` : null,
phone: (value) =>
!value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
payment_method: (value) =>
!value ? `${t("a payment method", { capfirst: true })} ${t("is required")}` : null,
},
});
@@ -61,8 +71,8 @@ export function Contract() {
if (!allProducts) {
return 0;
}
const values = Object.entries(inputForm.getValues());
return computePrices(values, allProducts, form?.shipments.length);
const productValues = Object.entries(inputForm.getValues().products);
return computePrices(productValues, allProducts, form?.shipments.length);
}, [inputForm, allProducts, form?.shipments]);
const inputRefs = useRef<Record<string, HTMLInputElement | null>>({
@@ -70,6 +80,7 @@ export function Contract() {
lastname: null,
email: null,
phone: null,
payment_method: null,
});
const isShipmentsMinimumValue = useCallback(() => {
@@ -77,7 +88,7 @@ export function Contract() {
const shipmentErrors = form.shipments
.map((shipment) => {
const total = computePrices(
Object.entries(inputForm.getValues()),
Object.entries(inputForm.getValues().products),
shipment.products,
);
if (total < (form?.minimum_shipment_value || 0)) {
@@ -122,8 +133,9 @@ export function Contract() {
}
if (inputForm.isValid() && isShipmentsMinimumValue()) {
const contract = {
...inputForm.getValues(),
form_id: form.id,
contract: withDefaultValues(inputForm.getValues()),
products: tranformProducts(withDefaultValues(inputForm.getValues().products)),
};
await createContractMutation.mutateAsync(contract);
} else {
@@ -253,6 +265,36 @@ export function Contract() {
</Accordion>
</>
) : null}
<Title order={3}>{t("payment method", { capfirst: true })}</Title>
<Select
label={t("payment method", { capfirst: true })}
placeholder={t("enter payment method", { capfirst: true })}
description={t("choose payment method", { capfirst: true })}
data={form.productor.payment_methods.map((payment) => ({
value: payment.name,
label: t(payment.name, { capfirst: true }),
}))}
{...inputForm.getInputProps("payment_method")}
ref={(el) => {
inputRefs.current.payment_method = el;
}}
/>
{inputForm.values.payment_method === "cheque" ? (
<ContractCheque
chequeOrder={
form?.productor?.payment_methods.find((el) => el.name === "cheque")
?.details || ""
}
price={price}
inputForm={inputForm}
/>
) : null}
{inputForm.values.payment_method === "transfer" ? (
<Text>
{t("for transfer method contact your referer or productor", { capfirst: true })}
</Text>
) : null}
<Space h="15vh"></Space>
<Overlay
bg={"lightGray"}
h="10vh"

View File

@@ -0,0 +1,120 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { useCreateContract, useGetContract, useGetContracts } from "@/services/api";
import { IconPlus } from "@tabler/icons-react";
import ContractRow from "@/components/Contracts/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { ContractModal } from "@/components/Contracts/Modal";
import { useCallback, useMemo } from "react";
import { type Contract, type ContractInputs } from "@/services/resources/contracts";
import ContractsFilters from "@/components/Contracts/Filter";
export default function Contracts() {
const [searchParams, setSearchParams] = useSearchParams();
// const location = useLocation();
// const navigate = useNavigate();
// const isCreate = location.pathname === "/dashboard/contracts/create";
// const isEdit = location.pathname.includes("/edit");
// const editId = useMemo(() => {
// if (isEdit) {
// return location.pathname.split("/")[3];
// }
// return null;
// }, [location, isEdit]);
// const closeModal = useCallback(() => {
// navigate(`/dashboard/contracts${searchParams ? `?${searchParams.toString()}` : ""}`);
// }, [navigate, searchParams]);
const { data: contracts, isPending } = useGetContracts(searchParams);
// const { data: currentContract } = useGetContract(Number(editId), {
// enabled: !!editId,
// });
const { data: allContracts } = useGetContracts();
const forms = useMemo(() => {
return allContracts
?.map((contract: Contract) => contract.form.name)
.filter((contract, index, array) => array.indexOf(contract) === index);
}, [allContracts]);
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;
});
},
[setSearchParams],
);
if (!contracts || isPending)
return (
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
return (
<Stack>
<Group justify="space-between">
<Title order={2}>{t("all referers", { capfirst: true })}</Title>
{/* <Tooltip label={t("create contract", { capfirst: true })}>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(
`/dashboard/contracts/create${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconPlus />
</ActionIcon>
</Tooltip>
<ContractModal
key={`${currentContract?.id}_create`}
opened={isCreate}
onClose={closeModal}
handleSubmit={handleCreateContract}
/>
<ContractModal
key={`${currentContract?.id}_edit`}
opened={isEdit}
onClose={closeModal}
currentContract={currentContract}
handleSubmit={handleEditContract}
/> */}
</Group>
<ContractsFilters
forms={forms || []}
filters={searchParams}
onFilterChange={onFilterChange}
/>
<ScrollArea type="auto">
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("name", { capfirst: true })}</Table.Th>
<Table.Th>{t("email", { capfirst: true })}</Table.Th>
<Table.Th>{t("payment method", { capfirst: true })}</Table.Th>
<Table.Th>{t("actions", { capfirst: true })}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{contracts.map((contract) => (
<ContractRow contract={contract} key={contract.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Stack>
);
}

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="contracts">{t("contracts", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="users">{t("users", { capfirst: true })}</Tabs.Tab>
</Tabs.List>
<Outlet />

View File

@@ -89,7 +89,7 @@ export default function Users() {
return (
<Stack>
<Group justify="space-between">
<Title order={2}>{t("all users", { capfirst: true })}</Title>
<Title order={2}>{t("all referers", { capfirst: true })}</Title>
<Tooltip label={t("create user", { capfirst: true })}>
<ActionIcon
onClick={(e) => {

View File

@@ -10,6 +10,7 @@ import Users from "@/pages/Users";
import Shipments from "./pages/Shipments";
import { Contract } from "./pages/Contract";
import { NotFound } from "./pages/NotFound";
import Contracts from "./pages/Contracts";
export const router = createBrowserRouter([
{
@@ -29,7 +30,7 @@ export const router = createBrowserRouter([
{ path: "products", Component: Products },
{ path: "products/create", Component: Products },
{ path: "products/:id/edit", Component: Products },
// { path: "templates", Component: Templates },
{ path: "contracts", Component: Contracts },
{ path: "users", Component: Users },
{ path: "users/create", Component: Users },
{ path: "users/:id/edit", Component: Users },

View File

@@ -15,7 +15,7 @@ import type {
} from "@/services/resources/productors";
import type { User, UserCreate, UserEditPayload } from "@/services/resources/users";
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
import type { ContractCreate } from "./resources/contracts";
import type { Contract, ContractCreate } from "./resources/contracts";
import { notifications } from "@mantine/notifications";
import { t } from "@/config/i18n";
@@ -563,6 +563,29 @@ export function useEditUser() {
});
}
export function useGetContracts(filters?: URLSearchParams): UseQueryResult<Contract[], Error> {
const queryString = filters?.toString();
return useQuery<Contract[]>({
queryKey: ["contracts", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`).then(
(res) => res.json(),
),
});
}
export function useGetContract(
id?: number,
options?: Partial<DefinedInitialDataOptions<Contract, Error, Contract, readonly unknown[]>>,
) {
return useQuery<Contract>({
queryKey: ["contract"],
queryFn: () => fetch(`${Config.backend_uri}/contracts/${id}`).then((res) => res.json()),
enabled: !!id,
...options,
});
}
export function useCreateContract() {
const queryClient = useQueryClient();
@@ -587,3 +610,31 @@ export function useCreateContract() {
},
});
}
export function useDeleteContract() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/contracts/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
onSuccess: async () => {
notifications.show({
title: t("success", { capfirst: true }),
message: t("successfully deleted contract", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ["contracts"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting contract`, { capfirst: true }),
color: "red",
});
},
});
}

View File

@@ -1,4 +1,72 @@
import type { Cheque } from "@/components/PaymentMethods/Cheque";
import type { Form } from "./forms";
import type { Product } from "./products";
import type { Shipment } from "./shipments";
export type Contract = {
id: number;
form_id: number;
products: ContractProduct;
form: Form;
firstname: string;
lastname: string;
email: string;
phone: string;
payment_method: string;
cheque_quantity: number;
};
export type ContractCreate = {
form_id: number;
contract: Record<string, string | number | null>;
firstname: string;
lastname: string;
email: string;
phone: string;
payment_method: string;
cheque_quantity: number;
products: ContractProductCreate[];
cheques: Cheque[];
};
export type ContractInputs = {
firstname: string;
lastname: string;
email: string;
phone: string;
products: Record<string, string | number>;
payment_method: string;
cheques: Cheque[];
cheque_quantity: number;
};
export type ContractProduct = {
id: number;
product_id: number;
shipment_id: number;
quantity: number;
contract: Contract;
product: Product;
shipment?: Shipment | null;
};
export type ContractProductCreate = {
product_id: number;
shipment_id: number | null;
quantity: number;
};
export function tranformProducts(
products: Record<string, string | number>,
): ContractProductCreate[] {
return Object.entries(products).map(([key, value]) => {
const quantity = value;
const parts = key.split("-");
const shipment_id = parts[0] === "planned" ? Number(parts[1]) : null;
const product_id = parts[0] === "planned" ? Number(parts[2]) : Number(parts[1]);
return {
quantity: Number(quantity),
shipment_id: shipment_id,
product_id: product_id,
};
});
}