Add min cheque value and view contract at the end of form flow
This commit is contained in:
@@ -10,37 +10,33 @@ export type FormCardProps = {
|
||||
};
|
||||
|
||||
export function FormCard({ form }: FormCardProps) {
|
||||
const contractBaseTemplate = useGetContractFileTemplate()
|
||||
const contractBaseTemplate = useGetContractFileTemplate();
|
||||
return (
|
||||
<Paper shadow="xl" p="xl" miw={{ base: "100vw", md: "25vw", lg: "20vw" }}>
|
||||
<Group justify="start" mb="md">
|
||||
<Tooltip
|
||||
label={t("download base template to print")}
|
||||
>
|
||||
<ActionIcon
|
||||
variant={"outline"}
|
||||
aria-label={t("download base template to print")}
|
||||
onClick={async () => {
|
||||
await contractBaseTemplate.mutateAsync(form.id)
|
||||
}}
|
||||
>
|
||||
<IconDownload/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={t("fill contract online")}
|
||||
>
|
||||
<ActionIcon
|
||||
variant={"outline"}
|
||||
aria-label={t("fill contract online")}
|
||||
component={Link}
|
||||
to={`/form/${form.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconExternalLink/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("download base template to print", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
variant={"outline"}
|
||||
aria-label={t("download base template to print", { capfirst: true })}
|
||||
onClick={async () => {
|
||||
await contractBaseTemplate.mutateAsync(form.id);
|
||||
}}
|
||||
>
|
||||
<IconDownload />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("fill contract online", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
variant={"outline"}
|
||||
aria-label={t("fill contract online", { capfirst: true })}
|
||||
component={Link}
|
||||
to={`/form/${form.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconExternalLink />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Box
|
||||
|
||||
@@ -43,21 +43,27 @@ export function ContractCheque({ inputForm, price, productor }: ContractChequePr
|
||||
}, [inputForm.values.cheque_quantity, price, inputForm.values.cheques]);
|
||||
|
||||
const paymentMethod = useMemo(() => {
|
||||
return productor?.payment_methods.find((el) => el.name === "cheque")
|
||||
return productor?.payment_methods.find((el) => el.name === "cheque");
|
||||
}, [productor]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={4}>{`${t("order name")} : ${paymentMethod?.details}`}</Title>
|
||||
<Title
|
||||
order={4}
|
||||
>{`${t("order name", { capfirst: true })} : ${paymentMethod?.details}`}</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 },
|
||||
{ capfirst: true, min: paymentMethod?.min || 1, max: paymentMethod?.max || 3 },
|
||||
)}
|
||||
min={1}
|
||||
max={paymentMethod?.max && paymentMethod?.max !== "" ? Number(paymentMethod?.max) : 3}
|
||||
min={
|
||||
paymentMethod?.min && paymentMethod?.min !== "" ? Number(paymentMethod?.min) : 1
|
||||
}
|
||||
max={
|
||||
paymentMethod?.max && paymentMethod?.max !== "" ? Number(paymentMethod?.max) : 3
|
||||
}
|
||||
{...inputForm.getInputProps(`cheque_quantity`)}
|
||||
/>
|
||||
<Group grow>
|
||||
@@ -67,12 +73,8 @@ export function ContractCheque({ inputForm, price, productor }: ContractChequePr
|
||||
label={t("cheque id", { capfirst: true })}
|
||||
placeholder={t("cheque id", { capfirst: true })}
|
||||
{...inputForm.getInputProps(`cheques.${index}.name`)}
|
||||
error={
|
||||
cheque.name == "" ?
|
||||
inputForm?.errors.cheques :
|
||||
null
|
||||
}
|
||||
/>
|
||||
error={cheque.name == "" ? inputForm?.errors.cheques : null}
|
||||
/>
|
||||
<NumberInput
|
||||
readOnly
|
||||
label={t("cheque value", { capfirst: true })}
|
||||
|
||||
17
frontend/src/components/PdfViewer/index.tsx
Normal file
17
frontend/src/components/PdfViewer/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
export type PdfViewerProps = {
|
||||
file: string;
|
||||
};
|
||||
|
||||
export function PdfViewer({ file }: PdfViewerProps) {
|
||||
return (
|
||||
<div onContextMenu={(e) => e.preventDefault()}>
|
||||
<iframe
|
||||
src={`${file.toString()}#toolbar=0&navpanes=0`}
|
||||
title="Contract PDF"
|
||||
width="100%"
|
||||
height="800px"
|
||||
style={{ border: 0 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,17 +48,19 @@ export function ProductorModal({
|
||||
!value ? `${t("address", { capfirst: true })} ${t("is required")}` : null,
|
||||
type: (value) =>
|
||||
!value ? `${t("type", { capfirst: true })} ${t("is required")}` : null,
|
||||
payment_methods: (value) =>
|
||||
value.length === 0 || value.some(
|
||||
(payment) =>
|
||||
payment.name === "cheque" &&
|
||||
payment.details === "") ?
|
||||
`${t("a payment method", { capfirst: true })} ${t("is required")}` : null,
|
||||
payment_methods: (value) =>
|
||||
value.length === 0 ||
|
||||
value.some((payment) => payment.name === "cheque" && payment.details === "")
|
||||
? `${t("a payment method", { capfirst: true })} ${t("is required")}`
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
const roleSelect = useMemo(() => {
|
||||
return loggedUser?.user?.roles?.map((role) => ({ value: String(role.name), label: role.name }));
|
||||
return loggedUser?.user?.roles?.map((role) => ({
|
||||
value: String(role.name),
|
||||
label: role.name,
|
||||
}));
|
||||
}, [loggedUser?.user?.roles]);
|
||||
|
||||
return (
|
||||
@@ -110,6 +112,7 @@ export function ProductorModal({
|
||||
name,
|
||||
details: "",
|
||||
max: "",
|
||||
min: "",
|
||||
}
|
||||
);
|
||||
}),
|
||||
@@ -124,10 +127,16 @@ export function ProductorModal({
|
||||
placeholder={t("order name", { capfirst: true })}
|
||||
{...form.getInputProps(`payment_methods.${index}.details`)}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("min cheque number", { capfirst: true })}
|
||||
placeholder={t("min cheque number", { capfirst: true })}
|
||||
description={t("can be empty default to 1", { capfirst: true })}
|
||||
{...form.getInputProps(`payment_methods.${index}.min`)}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("max cheque number", { capfirst: true })}
|
||||
placeholder={t("max cheque number", { capfirst: true })}
|
||||
description={t("can be empty default to 3", {capfirst: true})}
|
||||
description={t("can be empty default to 3", { capfirst: true })}
|
||||
{...form.getInputProps(`payment_methods.${index}.max`)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -19,15 +19,23 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { IconDownload, IconMail, IconPhone, IconUser } from "@tabler/icons-react";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } 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";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { ContractRecap } from "./recap";
|
||||
|
||||
export function Contract() {
|
||||
const { id } = useParams();
|
||||
const [pdfUrl, setPdfUrl] = useState<string | undefined>();
|
||||
const { data: form } = useGetForm(Number(id), { enabled: !!id });
|
||||
const [isRecapModalOpened, { open: openRecapModal, close: closeRecapModal }] =
|
||||
useDisclosure(false);
|
||||
const minCheques = useMemo(() => {
|
||||
return form?.productor?.payment_methods.find((pm) => pm.name === "cheque")?.min;
|
||||
}, [form]);
|
||||
const inputForm = useForm<ContractInputs>({
|
||||
initialValues: {
|
||||
firstname: "",
|
||||
@@ -50,11 +58,19 @@ export function Contract() {
|
||||
!value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
|
||||
payment_method: (value) =>
|
||||
!value ? `${t("a payment method", { capfirst: true })} ${t("is required")}` : null,
|
||||
cheques: (value, values) =>
|
||||
values.payment_method === "cheque" && value.some((val) => val.name == "") ? `${t("cheque id", {capfirst: true})} ${t("is required")}` : null,
|
||||
cheques: (value, values) =>
|
||||
values.payment_method === "cheque" && value.some((val) => val.name == "")
|
||||
? `${t("cheque id", { capfirst: true })} ${t("is required")}`
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!form) return;
|
||||
console.log("aze");
|
||||
inputForm.setFieldValue("cheque_quantity", Number(minCheques) || 1);
|
||||
}, [form]);
|
||||
|
||||
const createContractMutation = useCreateContract();
|
||||
|
||||
const productsRecurent = useMemo(() => {
|
||||
@@ -137,12 +153,15 @@ export function Contract() {
|
||||
const formValues = inputForm.getValues();
|
||||
const contract = {
|
||||
...formValues,
|
||||
cheque_quantity: formValues.payment_method === "cheque" ? formValues.cheque_quantity : 0,
|
||||
cheque_quantity:
|
||||
formValues.payment_method === "cheque" ? formValues.cheque_quantity : 0,
|
||||
form_id: form.id,
|
||||
products: tranformProducts(withDefaultValues(formValues.products)),
|
||||
};
|
||||
await createContractMutation.mutateAsync(contract);
|
||||
window.location.href = '/';
|
||||
const { blob } = await createContractMutation.mutateAsync(contract);
|
||||
const url = URL.createObjectURL(blob);
|
||||
setPdfUrl(url);
|
||||
openRecapModal();
|
||||
} else {
|
||||
const firstErrorField = Object.keys(errors.errors)[0];
|
||||
const ref = inputRefs.current[firstErrorField];
|
||||
@@ -155,6 +174,7 @@ export function Contract() {
|
||||
form,
|
||||
createContractMutation,
|
||||
withDefaultValues,
|
||||
setPdfUrl,
|
||||
]);
|
||||
|
||||
if (!form || !shipments || !productsRecurent)
|
||||
@@ -285,17 +305,14 @@ export function Contract() {
|
||||
}}
|
||||
/>
|
||||
{inputForm.values.payment_method === "cheque" ? (
|
||||
<ContractCheque
|
||||
productor={form?.productor}
|
||||
price={price}
|
||||
inputForm={inputForm}
|
||||
/>
|
||||
<ContractCheque productor={form?.productor} price={price} inputForm={inputForm} />
|
||||
) : null}
|
||||
{inputForm.values.payment_method === "transfer" ? (
|
||||
<Text>
|
||||
{t("for transfer method contact your referer or productor", { capfirst: true })}
|
||||
</Text>
|
||||
) : null}
|
||||
<ContractRecap opened={isRecapModalOpened} close={closeRecapModal} pdfUrl={pdfUrl} />
|
||||
<Space h="15vh"></Space>
|
||||
<Overlay
|
||||
bg={"lightGray"}
|
||||
@@ -316,10 +333,12 @@ export function Contract() {
|
||||
currency: "EUR",
|
||||
}).format(price)}
|
||||
</Text>
|
||||
<Button
|
||||
leftSection={<IconDownload/>}
|
||||
aria-label={t("submit contracts")} onClick={handleSubmit}>
|
||||
{t("submit contract", {capfirst: true})}
|
||||
<Button
|
||||
leftSection={<IconDownload />}
|
||||
aria-label={t("submit contracts")}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t("submit contract", { capfirst: true })}
|
||||
</Button>
|
||||
</Overlay>
|
||||
</Stack>
|
||||
|
||||
70
frontend/src/pages/Contract/recap.tsx
Normal file
70
frontend/src/pages/Contract/recap.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { PdfViewer } from "@/components/PdfViewer";
|
||||
import { t } from "@/config/i18n";
|
||||
import { Button, Checkbox, Loader, Modal, Stack, Text } from "@mantine/core";
|
||||
import { isNotEmpty, useForm } from "@mantine/form";
|
||||
import { useState } from "react";
|
||||
|
||||
export type ContractRecapProps = {
|
||||
opened: boolean;
|
||||
close: () => void;
|
||||
pdfUrl?: string;
|
||||
};
|
||||
|
||||
export function ContractRecap({ opened, close, pdfUrl }: ContractRecapProps) {
|
||||
const [viewPdf, setViewPdf] = useState<boolean>(false);
|
||||
const form = useForm({
|
||||
mode: "uncontrolled",
|
||||
initialValues: { terms: false },
|
||||
validate: {
|
||||
terms: isNotEmpty(t("you must accept terms and conditions", { capfirst: true })),
|
||||
},
|
||||
});
|
||||
|
||||
if (!pdfUrl)
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
size="100%"
|
||||
title={t("contract recap", { capfirst: true })}
|
||||
>
|
||||
<Loader></Loader>
|
||||
</Modal>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
size={"55rem"}
|
||||
title={t("contract recap", { capfirst: true })}
|
||||
>
|
||||
<Stack>
|
||||
<Text>{t("your contract has been submited", { capfirst: true })}</Text>
|
||||
<Button onClick={() => setViewPdf(!viewPdf)}>
|
||||
{t("show contract", { capfirst: true })}
|
||||
</Button>
|
||||
{viewPdf ? <PdfViewer file={pdfUrl} /> : null}
|
||||
<form
|
||||
onSubmit={form.onSubmit(() => {
|
||||
const link = document.createElement("a");
|
||||
link.href = pdfUrl;
|
||||
link.target = "_blank";
|
||||
link.click();
|
||||
URL.revokeObjectURL(pdfUrl);
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<Checkbox
|
||||
label={t("i have read and accept the mutual agreement", {
|
||||
capfirst: true,
|
||||
})}
|
||||
key={form.key("terms")}
|
||||
{...form.getInputProps("terms", { type: "checkbox" })}
|
||||
/>
|
||||
<Button type="submit">{t("download contract", { capfirst: true })}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default function Productors() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { data: productors, isPending } = useGetProductors(searchParams);
|
||||
|
||||
|
||||
const { data: allProductors } = useGetProductors();
|
||||
const isCreate = location.pathname === "/dashboard/productors/create";
|
||||
const isEdit = location.pathname.includes("/edit");
|
||||
@@ -58,11 +58,12 @@ export default function Productors() {
|
||||
async (productor: ProductorInputs) => {
|
||||
await createProductorMutation.mutateAsync({
|
||||
...productor,
|
||||
payment_methods: productor.payment_methods.map((payment) =>( {
|
||||
payment_methods: productor.payment_methods.map((payment) => ({
|
||||
name: payment.name,
|
||||
details: payment.details,
|
||||
max: payment.max === "" ? null : payment.max
|
||||
}))
|
||||
max: payment.max === "" ? null : payment.max,
|
||||
min: payment.min === "" ? null : payment.min,
|
||||
})),
|
||||
});
|
||||
closeModal();
|
||||
},
|
||||
@@ -76,11 +77,12 @@ export default function Productors() {
|
||||
id: id,
|
||||
productor: {
|
||||
...productor,
|
||||
payment_methods: productor.payment_methods.map((payment) =>( {
|
||||
name: payment.name,
|
||||
details: payment.details,
|
||||
max: payment.max === "" ? null : payment.max
|
||||
}))
|
||||
payment_methods: productor.payment_methods.map((payment) => ({
|
||||
name: payment.name,
|
||||
details: payment.details,
|
||||
max: payment.max === "" ? null : payment.max,
|
||||
min: payment.min === "" ? null : payment.min,
|
||||
})),
|
||||
},
|
||||
});
|
||||
closeModal();
|
||||
|
||||
@@ -26,10 +26,17 @@ import { notifications } from "@mantine/notifications";
|
||||
import { t } from "@/config/i18n";
|
||||
|
||||
export async function refreshToken() {
|
||||
return await fetch(`${Config.backend_uri}/auth/refresh`, {method: "POST", credentials: "include"});
|
||||
return await fetch(`${Config.backend_uri}/auth/refresh`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchWithAuth(input: RequestInfo, options?: RequestInit, redirect: boolean = true) {
|
||||
export async function fetchWithAuth(
|
||||
input: RequestInfo,
|
||||
options?: RequestInit,
|
||||
redirect: boolean = true,
|
||||
) {
|
||||
const res = await fetch(input, {
|
||||
credentials: "include",
|
||||
...options,
|
||||
@@ -38,11 +45,10 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit, r
|
||||
if (res.status === 401) {
|
||||
const refresh = await refreshToken();
|
||||
if (refresh.status == 400 || refresh.status == 401) {
|
||||
if (redirect)
|
||||
window.location.href = `/?sessionExpired=True`;
|
||||
|
||||
if (redirect) window.location.href = `/?sessionExpired=True`;
|
||||
|
||||
const error = new Error("Unauthorized");
|
||||
error.cause = 401
|
||||
error.cause = 401;
|
||||
throw error;
|
||||
}
|
||||
const newRes = await fetch(input, {
|
||||
@@ -188,9 +194,7 @@ export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Prod
|
||||
queryFn: () =>
|
||||
fetchWithAuth(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json()
|
||||
),
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,7 +330,9 @@ export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], E
|
||||
return useQuery<Form[]>({
|
||||
queryKey: ["forms", queryString],
|
||||
queryFn: () =>
|
||||
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`).then((res) => res.json()),
|
||||
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`).then((res) =>
|
||||
res.json(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,7 +341,9 @@ export function useGetReferentForms(filters?: URLSearchParams): UseQueryResult<F
|
||||
return useQuery<Form[]>({
|
||||
queryKey: ["forms", queryString],
|
||||
queryFn: () =>
|
||||
fetchWithAuth(`${Config.backend_uri}/forms/referents${filters ? `?${queryString}` : ""}`).then((res) => res.json()),
|
||||
fetchWithAuth(
|
||||
`${Config.backend_uri}/forms/referents${filters ? `?${queryString}` : ""}`,
|
||||
).then((res) => res.json()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -723,8 +731,9 @@ export function useGetContractFile() {
|
||||
export function useGetContractFileTemplate() {
|
||||
return useMutation({
|
||||
mutationFn: async (form_id: number) => {
|
||||
const res = await fetchWithAuth(`${Config.backend_uri}/contracts/${form_id}/base`)
|
||||
.then((res) => res);
|
||||
const res = await fetchWithAuth(`${Config.backend_uri}/contracts/${form_id}/base`).then(
|
||||
(res) => res,
|
||||
);
|
||||
|
||||
if (!res.ok) throw new Error();
|
||||
const blob = await res.blob();
|
||||
@@ -746,7 +755,6 @@ export function useGetContractFileTemplate() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function useGetRecap() {
|
||||
return useMutation({
|
||||
mutationFn: async (form_id: number) => {
|
||||
@@ -822,13 +830,7 @@ export function useCreateContract() {
|
||||
: `contract_${newContract.form_id}.pdf`;
|
||||
return { blob, filename };
|
||||
},
|
||||
onSuccess: async ({ blob, filename }) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["contracts"] });
|
||||
},
|
||||
});
|
||||
@@ -880,10 +882,14 @@ export function useCurrentUser() {
|
||||
return useQuery<UserLogged>({
|
||||
queryKey: ["currentUser"],
|
||||
queryFn: () => {
|
||||
return fetchWithAuth(`${Config.backend_uri}/auth/user/me`, {
|
||||
credentials: "include",
|
||||
}, false).then((res) => res.json());
|
||||
return fetchWithAuth(
|
||||
`${Config.backend_uri}/auth/user/me`,
|
||||
{
|
||||
credentials: "include",
|
||||
},
|
||||
false,
|
||||
).then((res) => res.json());
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type PaymentMethod = {
|
||||
name: string;
|
||||
details: string;
|
||||
max: number | string | null;
|
||||
min: number | string | null;
|
||||
};
|
||||
|
||||
export type Productor = {
|
||||
|
||||
Reference in New Issue
Block a user