add logout logic and wip recap
This commit is contained in:
@@ -28,6 +28,14 @@ export default function ContractRow({ contract }: ContractRowProps) {
|
||||
<Table.Td>
|
||||
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
`${Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(contract.total_price)}`
|
||||
}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label={t("download contract", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
|
||||
@@ -41,7 +41,6 @@ export function Navbar() {
|
||||
href={`${Config.backend_uri}/auth/logout`}
|
||||
className={"navLink"}
|
||||
aria-label={t("logout", { capfirst: true })}
|
||||
|
||||
>
|
||||
{t("logout", { capfirst: true })}
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from "@/config/i18n";
|
||||
import type { ContractInputs } from "@/services/resources/contracts";
|
||||
import { Group, NumberInput, Stack, TextInput, Title } from "@mantine/core";
|
||||
import { Group, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import type { UseFormReturnType } from "@mantine/form";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -56,13 +56,18 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque
|
||||
{...inputForm.getInputProps(`cheque_quantity`)}
|
||||
/>
|
||||
<Group grow>
|
||||
{inputForm.values.cheques.map((_cheque, index) => (
|
||||
{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`)}
|
||||
/>
|
||||
error={
|
||||
cheque.name == "" ?
|
||||
<Text size="sm" c="red">{inputForm?.errors.cheques}</Text> :
|
||||
null
|
||||
}
|
||||
/>
|
||||
<NumberInput
|
||||
readOnly
|
||||
label={t("cheque value", { capfirst: true })}
|
||||
|
||||
@@ -21,7 +21,6 @@ export default function ProductorsFilter({
|
||||
const defaultTypes = useMemo(() => {
|
||||
return filters.getAll("types");
|
||||
}, [filters]);
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<MultiSelect
|
||||
|
||||
@@ -46,6 +46,12 @@ 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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -89,6 +95,7 @@ export function ProductorModal({
|
||||
clearable
|
||||
searchable
|
||||
value={form.values.payment_methods.map((p) => p.name)}
|
||||
error={form.errors.payment_methods}
|
||||
onChange={(names) => {
|
||||
form.setFieldValue(
|
||||
"payment_methods",
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Contract() {
|
||||
email: "",
|
||||
phone: "",
|
||||
payment_method: "",
|
||||
cheque_quantity: 1,
|
||||
cheque_quantity: 0,
|
||||
cheques: [],
|
||||
products: {},
|
||||
},
|
||||
@@ -50,6 +50,8 @@ 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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useGetAllContractFile, useGetContracts } from "@/services/api";
|
||||
import { IconDownload } from "@tabler/icons-react";
|
||||
import { useGetAllContractFile, useGetContracts, useGetRecap } from "@/services/api";
|
||||
import { IconDownload, IconTableExport } from "@tabler/icons-react";
|
||||
import ContractRow from "@/components/Contracts/Row";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { ContractModal } from "@/components/Contracts/Modal";
|
||||
@@ -14,7 +14,9 @@ export default function Contracts() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const getAllContractFilesMutation = useGetAllContractFile();
|
||||
const getRecapMutation = useGetRecap();
|
||||
const isdownload = location.pathname.includes("/download");
|
||||
const isrecap = location.pathname.includes("/export");
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
navigate(`/dashboard/contracts${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
@@ -52,6 +54,13 @@ export default function Contracts() {
|
||||
[getAllContractFilesMutation],
|
||||
);
|
||||
|
||||
const handleDownloadRecap = useCallback(
|
||||
async (id: number) => {
|
||||
await getRecapMutation.mutateAsync(id);
|
||||
},
|
||||
[getAllContractFilesMutation],
|
||||
)
|
||||
|
||||
if (!contracts || isPending)
|
||||
return (
|
||||
<Group align="center" justify="center" h="80vh" w="100%">
|
||||
@@ -63,23 +72,45 @@ export default function Contracts() {
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>{t("all contracts", { capfirst: true })}</Title>
|
||||
<Tooltip label={t("download contracts", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/download${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}}
|
||||
<Group>
|
||||
<Tooltip label={t("download contracts", { capfirst: true })}>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/download${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<IconDownload />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={t("download recap", { capfirst: true })}
|
||||
>
|
||||
<IconDownload />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/export${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<IconTableExport />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<ContractModal
|
||||
opened={isdownload}
|
||||
onClose={closeModal}
|
||||
handleSubmit={handleDownloadContracts}
|
||||
/>
|
||||
<ContractModal
|
||||
opened={isrecap}
|
||||
onClose={closeModal}
|
||||
handleSubmit={handleDownloadRecap}
|
||||
/>
|
||||
</Group>
|
||||
<ContractsFilters
|
||||
forms={forms || []}
|
||||
@@ -94,6 +125,7 @@ export default function Contracts() {
|
||||
<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("total price", { capfirst: true })}</Table.Th>
|
||||
<Table.Th>{t("actions", { capfirst: true })}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Tabs } from "@mantine/core";
|
||||
import { t } from "@/config/i18n";
|
||||
import { Link, Outlet, useLocation, useNavigate } from "react-router";
|
||||
import { useAuth } from "@/services/auth/AuthProvider";
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const {loggedUser} = useAuth();
|
||||
return (
|
||||
<Tabs
|
||||
w={{ base: "100%", md: "80%", lg: "60%" }}
|
||||
@@ -21,7 +22,11 @@ export default function Dashboard() {
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/forms" {...props}></Link>)} value="forms">{t("forms", { capfirst: true })}</Tabs.Tab>
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/shipments" {...props}></Link>)} value="shipments">{t("shipments", { capfirst: true })}</Tabs.Tab>
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/contracts" {...props}></Link>)} value="contracts">{t("contracts", { capfirst: true })}</Tabs.Tab>
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/users" {...props}></Link>)} value="users">{t("users", { capfirst: true })}</Tabs.Tab>
|
||||
{
|
||||
loggedUser?.user?.roles && loggedUser?.user?.roles?.length > 5 ?
|
||||
<Tabs.Tab renderRoot={(props) => (<Link to="/dashboard/users" {...props}></Link>)} value="users">{t("users", { capfirst: true })}</Tabs.Tab> :
|
||||
null
|
||||
}
|
||||
</Tabs.List>
|
||||
<Outlet />
|
||||
</Tabs>
|
||||
|
||||
@@ -3,21 +3,20 @@ import {
|
||||
Accordion,
|
||||
ActionIcon,
|
||||
Blockquote,
|
||||
Group,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TableOfContents,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconDownload,
|
||||
IconEdit,
|
||||
IconInfoCircle,
|
||||
IconLink,
|
||||
IconPlus,
|
||||
IconTableExport,
|
||||
IconTestPipe,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -77,7 +76,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("productors") })}
|
||||
{t("button in top right of the page", { section: t("productors") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -127,7 +126,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("products") })}
|
||||
{t("button in top right of the page", { section: t("products") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -178,7 +177,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("forms") })}
|
||||
{t("button in top right of the page", { section: t("forms") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -225,7 +224,7 @@ export function Help() {
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus />
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top left of the page", { section: t("shipments") })}
|
||||
{t("button in top right of the page", { section: t("shipments") })}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
|
||||
@@ -243,6 +242,41 @@ export function Help() {
|
||||
</Text>
|
||||
</Blockquote>
|
||||
</Stack>
|
||||
<Title order={3}>{t("export contracts", {capfirst: true})}</Title>
|
||||
<Stack>
|
||||
<Text>
|
||||
{t("to export contracts submissions before sending to the productor go to the contracts section", {capfirst: true})}
|
||||
<ActionIcon
|
||||
ml="4"
|
||||
size="xs"
|
||||
component={Link}
|
||||
to="/dashboard/contracts"
|
||||
aria-label={t("link to the section", {
|
||||
capfirst: true,
|
||||
section: t("shipments"),
|
||||
})}
|
||||
style={{ cursor: "pointer", alignSelf: "center" }}
|
||||
>
|
||||
<IconLink />
|
||||
</ActionIcon>
|
||||
</Text>
|
||||
<Text>{t("in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract", {capfirst: true})}</Text>
|
||||
<Text>
|
||||
{t("you can download all contracts for your form using the export all", {capfirst: true})}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconDownload/>
|
||||
</ActionIcon>{" "}
|
||||
{t("button in top right of the page", { section: t("contracts") })}{" "}
|
||||
{t("in the same corner you can download a recap by clicking on the button", {capfirst: true})}{" "}
|
||||
<ActionIcon size="sm">
|
||||
<IconTableExport/>
|
||||
</ActionIcon>{" "}
|
||||
|
||||
</Text>
|
||||
<Text>
|
||||
{t("once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page", {capfirst: true})}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Title order={3}>{t("glossary", { capfirst: true })}</Title>
|
||||
<Stack>
|
||||
<Title order={4} fw={700}>
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import { Flex, Stack, Text } from "@mantine/core";
|
||||
import { useGetForms } from "@/services/api";
|
||||
import { FormCard } from "@/components/Forms/Card";
|
||||
import type { Form } from "@/services/resources/forms";
|
||||
import { t } from "@/config/i18n";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { useEffect } from "react";
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
|
||||
export function Home() {
|
||||
const { data: allForms } = useGetForms();
|
||||
const { data: allForms } = useGetForms(new URLSearchParams("?current_season=true"));
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get("sessionExpired")) {
|
||||
showNotification({
|
||||
title: t("session expired", {capfirst: true}),
|
||||
message: t("your session has expired please log in again", {capfirst: true}),
|
||||
color: "red",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
if (searchParams.get("userNotAllowed")) {
|
||||
showNotification({
|
||||
title: t("user not allowed", {capfirst: true}),
|
||||
message: t("your keycloak user has no roles, please contact your administrator", {capfirst: true}),
|
||||
color: "red",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
return (
|
||||
<Flex gap="md" wrap="wrap" justify="center">
|
||||
{allForms && allForms?.length > 0 ? (
|
||||
allForms.map((form: Form) => <FormCard form={form} key={form.id} />)
|
||||
) : (
|
||||
<Text mt="lg" size="lg">
|
||||
{t("there is no contract for now", { capfirst: true })}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Stack mt="lg">
|
||||
<Flex gap="md" wrap="wrap" justify="center">
|
||||
{allForms && allForms?.length > 0 ? (
|
||||
allForms.map((form: Form) => <FormCard form={form} key={form.id} />)
|
||||
) : (
|
||||
<Text mt="lg" size="lg">
|
||||
{t("there is no contract for now", { capfirst: true })}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ export default function Productors() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
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");
|
||||
|
||||
@@ -29,15 +31,13 @@ export default function Productors() {
|
||||
return null;
|
||||
}, [location, isEdit]);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
const { data: productors, isPending } = useGetProductors(searchParams);
|
||||
const { data: currentProductor } = useGetProductor(Number(editId), {
|
||||
enabled: !!editId,
|
||||
});
|
||||
const { data: allProductors } = useGetProductors();
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
|
||||
}, [navigate, searchParams]);
|
||||
|
||||
const names = useMemo(() => {
|
||||
return allProductors
|
||||
|
||||
@@ -17,7 +17,6 @@ export default function Products() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isCreate = location.pathname === "/dashboard/products/create";
|
||||
const isEdit = location.pathname.includes("/edit");
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const router = createBrowserRouter([
|
||||
{ path: "products/:id/edit", Component: Products },
|
||||
{ path: "contracts", Component: Contracts },
|
||||
{ path: "contracts/download", Component: Contracts },
|
||||
{ path: "contracts/export", Component: Contracts },
|
||||
{ path: "users", Component: Users },
|
||||
{ path: "users/create", Component: Users },
|
||||
{ path: "users/:id/edit", Component: Users },
|
||||
|
||||
@@ -38,7 +38,8 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
|
||||
if (res.status === 401) {
|
||||
const refresh = await refreshToken();
|
||||
if (refresh.status == 400 || refresh.status == 401) {
|
||||
window.location.href = `${Config.backend_uri}/auth/logout`;
|
||||
window.location.href = `/?sessionExpired=True`;
|
||||
|
||||
const error = new Error("Unauthorized");
|
||||
error.cause = 401
|
||||
throw error;
|
||||
@@ -49,6 +50,9 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
|
||||
});
|
||||
return newRes;
|
||||
}
|
||||
if (res.status == 403) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -693,7 +697,33 @@ export function useGetContractFile() {
|
||||
disposition && disposition?.includes("filename=")
|
||||
? disposition.split("filename=")[1].replace(/"/g, "")
|
||||
: `contract_${id}.pdf`;
|
||||
console.log(disposition);
|
||||
return { blob, filename };
|
||||
},
|
||||
onSuccess: ({ blob, filename }) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetRecap() {
|
||||
return useMutation({
|
||||
mutationFn: async (form_id: number) => {
|
||||
const res = await fetchWithAuth(`${Config.backend_uri}/contracts/${form_id}/recap`, {
|
||||
credentials: "include",
|
||||
}).then((res) => res);
|
||||
|
||||
if (!res.ok) throw new Error();
|
||||
const blob = await res.blob();
|
||||
const disposition = res.headers.get("Content-Disposition");
|
||||
const filename =
|
||||
disposition && disposition?.includes("filename=")
|
||||
? disposition.split("filename=")[1].replace(/"/g, "")
|
||||
: `contract_recap_${form_id}.odt`;
|
||||
return { blob, filename };
|
||||
},
|
||||
onSuccess: ({ blob, filename }) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ export type Contract = {
|
||||
phone: string;
|
||||
payment_method: string;
|
||||
cheque_quantity: number;
|
||||
total_price: number;
|
||||
};
|
||||
|
||||
export type ContractCreate = {
|
||||
|
||||
@@ -15,7 +15,7 @@ export type User = {
|
||||
name: string;
|
||||
email: string;
|
||||
products: Product[];
|
||||
roles: Role[];
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export type UserInputs = {
|
||||
|
||||
Reference in New Issue
Block a user