add contract storage fix various bugs and translations
This commit is contained in:
30
frontend/src/components/Contracts/Filter/index.tsx
Normal file
30
frontend/src/components/Contracts/Filter/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
84
frontend/src/components/Contracts/Modal/index.tsx
Normal file
84
frontend/src/components/Contracts/Modal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
frontend/src/components/Contracts/Row/index.tsx
Normal file
55
frontend/src/components/Contracts/Row/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
78
frontend/src/components/PaymentMethods/Cheque/index.tsx
Normal file
78
frontend/src/components/PaymentMethods/Cheque/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
120
frontend/src/pages/Contracts/index.tsx
Normal file
120
frontend/src/pages/Contracts/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user