Add authentification

This commit is contained in:
2026-02-17 00:54:36 +01:00
parent ab98ba81c8
commit a8c8c489da
31 changed files with 1118 additions and 451 deletions

View File

@@ -0,0 +1,20 @@
import { useCurrentUser } from "@/services/api";
import { Group, Loader } from "@mantine/core";
import { Navigate, Outlet } from "react-router";
export function Auth() {
const { data: userLogged, isLoading } = useCurrentUser();
if (!userLogged && isLoading)
return (
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
if (!userLogged?.logged) {
return <Navigate to="/" replace />;
}
return <Outlet />;
}

View File

@@ -1,50 +1,57 @@
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { Button, Group, Modal, Select, 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";
import { IconCancel, IconDownload } from "@tabler/icons-react";
import { useGetForms } from "@/services/api";
import { useMemo } from "react";
export type ContractModalProps = ModalBaseProps & {
currentContract?: Contract;
handleSubmit: (contract: ContractInputs, id?: number) => void;
handleSubmit: (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,
// },
export type ContractDownloadInputs = {
form_id: number;
};
export function ContractModal({ opened, onClose, handleSubmit }: ContractModalProps) {
const { data: allForms } = useGetForms();
const form = useForm({
initialValues: {
form_id: null,
},
validate: {
form_id: (value) =>
!value ? `${t("a form", { capfirst: true })} ${t("is required")}` : null,
},
});
const formSelect = useMemo(() => {
return allForms?.map((form) => ({
value: String(form.id),
label: `${form.season} ${form.name}`,
}));
}, [allForms]);
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"
<Modal
opened={opened}
onClose={onClose}
title={t("download contracts", { capfirst: true })}
>
<Select
label={t("form", { capfirst: true })}
placeholder={t("select a form", { capfirst: true })}
description={t(
"by selecting a form here you can download all contracts of your form",
{ capfirst: true },
)}
nothingFoundMessage={t("nothing found", { capfirst: true })}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
label={t("contract email", { capfirst: true })}
placeholder={t("contract email", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps("email")}
clearable
allowDeselect
searchable
data={formSelect || []}
{...form.getInputProps("form_id")}
/>
<Group mt="sm" justify="space-between">
<Button
@@ -61,22 +68,16 @@ export function ContractModal({
</Button>
<Button
variant="filled"
aria-label={
currentContract
? t("edit contract", { capfirst: true })
: t("create contract", { capfirst: true })
}
leftSection={currentContract ? <IconEdit /> : <IconPlus />}
aria-label={t("download contracts")}
leftSection={<IconDownload />}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentContract?.id);
handleSubmit(Number(form.values.form_id));
}
}}
>
{currentContract
? t("edit contract", { capfirst: true })
: t("create contract", { capfirst: true })}
{t("download contracts", { capfirst: true })}
</Button>
</Group>
</Modal>

View File

@@ -10,24 +10,17 @@ export type ContractRowProps = {
};
export default function ContractRow({ contract }: ContractRowProps) {
// const [searchParams] = useSearchParams();
const deleteMutation = useDeleteContract();
// const navigate = useNavigate();
const {refetch, isFetching} = useGetContractFile(contract.id)
const handleDownload = useCallback(async () => {
const { data } = await refetch();
if (!data)
return;
const getContractMutation = useGetContractFile();
const url = URL.createObjectURL(data.blob);
const link = document.createElement("a");
link.href = url;
link.download = data.filename;
link.click();
URL.revokeObjectURL(url);
}, [useGetContractFile])
const handleDownload = useCallback(async () => {
getContractMutation.mutateAsync(contract.id);
}, [useGetContractFile, contract, contract.id]);
return (
<Table.Tr key={contract.id}>
<Table.Td>
{contract.form.name} {contract.form.season}
</Table.Td>
<Table.Td>
{contract.firstname} {contract.lastname}
</Table.Td>
@@ -36,32 +29,16 @@ export default function ContractRow({ contract }: ContractRowProps) {
{contract.cheque_quantity > 0 && contract.cheque_quantity} {contract.payment_method}
</Table.Td>
<Table.Td>
{/* <Tooltip label={t("edit contract", { capfirst: true })}>
<Tooltip label={t("download contract", { capfirst: true })}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(
`/dashboard/contracts/${contract.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
handleDownload();
}}
>
<IconEdit />
</ActionIcon>
</Tooltip> */}
<Tooltip
label={t("download contract")}
>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
handleDownload()
}}
>
<IconDownload/>
<IconDownload />
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove contract", { capfirst: true })}>

View File

@@ -1,31 +1,52 @@
import { NavLink } from "react-router";
import { t } from "@/config/i18n";
import "./index.css";
import { Group } from "@mantine/core";
import { Button, Group, Loader } from "@mantine/core";
import { Config } from "@/config/config";
import { useCurrentUser, useLogoutUser } from "@/services/api";
export function Navbar() {
const { data: user, isLoading } = useCurrentUser();
const logout = useLogoutUser();
if (!user && isLoading) {
return (
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
}
return (
<nav>
<Group>
<NavLink className={"navLink"} aria-label={t("home")} to="/">
{t("home", { capfirst: true })}
</NavLink>
{user?.logged ? (
<NavLink className={"navLink"} aria-label={t("dashboard")} to="/dashboard/help">
{t("dashboard", { capfirst: true })}
</NavLink>
) : null}
</Group>
{!user?.logged ? (
<NavLink
className={"navLink"}
aria-label={t("dashboard")}
to="/dashboard/help"
aria-label={t("login with keycloak", { capfirst: true })}
to={`${Config.backend_uri}/auth/login`}
>
{t("dashboard", { capfirst: true })}
{t("login with keycloak", { capfirst: true })}
</NavLink>
</Group>
<NavLink
className={"navLink"}
aria-label={t("login with keycloak", { capfirst: true })}
to={`${Config.backend_uri}/auth/login`}
>
{t("login with keycloak", { capfirst: true })}
</NavLink>
) : (
<Button
className={"navLink"}
aria-label={t("logout", { capfirst: true })}
onClick={() => {
logout.mutate();
}}
>
{t("logout", { capfirst: true })}
</Button>
)}
</nav>
);
}

View File

@@ -3,6 +3,7 @@ import {
Group,
Modal,
MultiSelect,
Select,
TextInput,
Title,
type ModalBaseProps,
@@ -15,6 +16,8 @@ import {
type Productor,
type ProductorInputs,
} from "@/services/resources/productors";
import { useMemo } from "react";
import { useGetRoles } from "@/services/api";
export type ProductorModalProps = ModalBaseProps & {
currentProductor?: Productor;
@@ -27,6 +30,8 @@ export function ProductorModal({
currentProductor,
handleSubmit,
}: ProductorModalProps) {
const { data: allRoles } = useGetRoles();
const form = useForm<ProductorInputs>({
initialValues: {
name: currentProductor?.name ?? "",
@@ -44,6 +49,10 @@ export function ProductorModal({
},
});
const roleSelect = useMemo(() => {
return allRoles?.map((role) => ({ value: String(role.name), label: role.name }));
}, [allRoles]);
return (
<Modal opened={opened} onClose={onClose} title={t("create productor", { capfirst: true })}>
<Title order={4}>{t("Informations", { capfirst: true })}</Title>
@@ -54,11 +63,14 @@ export function ProductorModal({
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
<Select
label={t("productor type", { capfirst: true })}
placeholder={t("productor type", { capfirst: true })}
radius="sm"
withAsterisk
clearable
searchable
data={roleSelect || []}
{...form.getInputProps("type")}
/>
<TextInput

View File

@@ -19,7 +19,6 @@ import {
} from "@/services/resources/products";
import { useMemo } from "react";
import { useGetProductors } from "@/services/api";
import { InputLabel } from "@/components/Label";
export type ProductModalProps = ModalBaseProps & {
currentProduct?: Product;
@@ -90,7 +89,10 @@ export function ProductModal({ opened, onClose, currentProduct, handleSubmit }:
label={t("product type", { capfirst: true })}
placeholder={t("product type", { capfirst: true })}
radius="sm"
description={t("a product type define the way it will be organized on the final contract form (showed to users) it can be reccurent or occassional. Recurrent products will be set for all shipments if selected by user, Occasional products can be choosen for each shipments", {capfirst: true})}
description={t(
"a product type define the way it will be organized on the final contract form (showed to users) it can be reccurent or occassional. Recurrent products will be set for all shipments if selected by user, Occasional products can be choosen for each shipments",
{ capfirst: true },
)}
searchable
clearable
withAsterisk

View File

@@ -1,8 +1,19 @@
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import {
Button,
Group,
Modal,
MultiSelect,
Select,
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 User, type UserInputs } from "@/services/resources/users";
import { useGetRoles } from "@/services/api";
import { useMemo } from "react";
export type UserModalProps = ModalBaseProps & {
currentUser?: User;
@@ -10,10 +21,12 @@ export type UserModalProps = ModalBaseProps & {
};
export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserModalProps) {
const { data: allRoles } = useGetRoles();
const form = useForm<UserInputs>({
initialValues: {
name: currentUser?.name ?? "",
email: currentUser?.email ?? "",
role_names: currentUser?.roles.map((role) => role.name) ?? [],
},
validate: {
name: (value) =>
@@ -23,6 +36,10 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
},
});
const roleSelect = useMemo(() => {
return allRoles?.map((role) => ({ value: String(role.name), label: role.name }));
}, [allRoles]);
return (
<Modal opened={opened} onClose={onClose} title={t("create user", { capfirst: true })}>
<Title order={4}>{t("informations", { capfirst: true })}</Title>
@@ -40,6 +57,16 @@ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserMo
withAsterisk
{...form.getInputProps("email")}
/>
<MultiSelect
label={t("user roles", { capfirst: true })}
placeholder={t("user roles", { capfirst: true })}
radius="sm"
withAsterisk
clearable
searchable
data={roleSelect || []}
{...form.getInputProps("role_names")}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"

View File

@@ -250,8 +250,8 @@ export function Contract() {
) : null}
{shipments.some((shipment) => shipment.products.length > 0) ? (
<>
<Title order={3}>{t("occasional products")}</Title>
<Text>{t("select products per shipment")}</Text>
<Title order={3}>{t("occasional products", { capfirst: true })}</Title>
<Text>{t("select products per shipment", { capfirst: true })}</Text>
<Accordion defaultValue={"0"}>
{shipments.map((shipment, index) => (
<ShipmentForm

View File

@@ -1,37 +1,26 @@
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 { useGetAllContractFile, useGetContracts } from "@/services/api";
import { IconDownload } 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 { type Contract } 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 location = useLocation();
const navigate = useNavigate();
const getAllContractFilesMutation = useGetAllContractFile();
const isdownload = location.pathname.includes("/download");
// 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 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();
@@ -56,6 +45,13 @@ export default function Contracts() {
[setSearchParams],
);
const handleDownloadContracts = useCallback(
async (id: number) => {
await getAllContractFilesMutation.mutateAsync(id);
},
[getAllContractFilesMutation],
);
if (!contracts || isPending)
return (
<Group align="center" justify="center" h="80vh" w="100%">
@@ -66,32 +62,24 @@ export default function Contracts() {
return (
<Stack>
<Group justify="space-between">
<Title order={2}>{t("all referers", { capfirst: true })}</Title>
{/* <Tooltip label={t("create contract", { capfirst: true })}>
<Title order={2}>{t("all contracts", { capfirst: true })}</Title>
<Tooltip label={t("download contracts", { capfirst: true })}>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(
`/dashboard/contracts/create${searchParams ? `?${searchParams.toString()}` : ""}`,
`/dashboard/contracts/download${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconPlus />
<IconDownload />
</ActionIcon>
</Tooltip>
<ContractModal
key={`${currentContract?.id}_create`}
opened={isCreate}
opened={isdownload}
onClose={closeModal}
handleSubmit={handleCreateContract}
handleSubmit={handleDownloadContracts}
/>
<ContractModal
key={`${currentContract?.id}_edit`}
opened={isEdit}
onClose={closeModal}
currentContract={currentContract}
handleSubmit={handleEditContract}
/> */}
</Group>
<ContractsFilters
forms={forms || []}
@@ -102,6 +90,7 @@ export default function Contracts() {
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("form", { capfirst: true })}</Table.Th>
<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>

View File

@@ -1,14 +1,37 @@
import { t } from "@/config/i18n";
import { Accordion, ActionIcon, Blockquote, Group, NumberInput, Paper, Select, Stack, TableOfContents, Text, TextInput, Title } from "@mantine/core";
import { IconEdit, IconInfoCircle, IconLink, IconPlus, IconTestPipe, IconX } from "@tabler/icons-react";
import {
Accordion,
ActionIcon,
Blockquote,
Group,
NumberInput,
Paper,
Select,
Stack,
TableOfContents,
Text,
TextInput,
Title,
} from "@mantine/core";
import {
IconEdit,
IconInfoCircle,
IconLink,
IconPlus,
IconTestPipe,
IconX,
} from "@tabler/icons-react";
import { Link } from "react-router";
export function Help() {
return (
<Stack>
<Title order={2}>{t("how to use dashboard", {capfirst: true})}</Title>
<Title order={2}>{t("how to use dashboard", { capfirst: true })}</Title>
<Text>
{t("dashboard is for referers only, with this dashboard you can create productors, products, forms and shipments", {capfirst: true})}
{t(
"dashboard is for referers only, with this dashboard you can create productors, products, forms and shipments",
{ capfirst: true },
)}
</Text>
<TableOfContents
variant="filled"
@@ -16,144 +39,255 @@ export function Help() {
size="sm"
radius="sm"
scrollSpyOptions={{
selector: 'h3, h4',
selector: "h3, h4",
}}
getControlProps={({ data }) => ({
onClick: () => data.getNode().scrollIntoView(),
children: data.value,
})}
/>
<Title order={3}>{t("creation order", {capfirst: true})}</Title>
<Title order={3}>{t("creation order", { capfirst: true })}</Title>
<Stack gap="1">
<Title order={4}>{t("a productor", {capfirst: true})}</Title>
<Title order={4}>{t("a productor", { capfirst: true })}</Title>
<Text>
{t("start to create a productor in the productors section", {capfirst: true})}
{t("start to create a productor in the productors section", { capfirst: true })}
<ActionIcon
ml="4"
size="xs"
component={Link}
to="/dashboard/productors"
aria-label={t("link to the section", {capfirst: true, section: t("productors")})}
style={{cursor: "pointer", alignSelf: "center"}}
><IconLink/></ActionIcon>
</Text>
<Text>{t("a productor can be edited if its informations change, it should not be recreated for each contracts", {capfirst: true})}</Text>
<Blockquote
mt="md"
icon={<IconInfoCircle/>}
>
<Text>{t("to add a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("productors")})}</Text>
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
</Blockquote>
</Stack>
<Stack gap="1">
<Title order={4}>{t("the products", {capfirst: true})}</Title>
<Text>
{t("add all products linked to this productor in the products section", {capfirst: true})}
<ActionIcon
ml="4"
size="xs"
component={Link}
to="/dashboard/products"
aria-label={t("link to the section", {capfirst: true, section: t("products")})}
style={{cursor: "pointer", alignSelf: "center"}}
><IconLink/></ActionIcon>
</Text>
<Text>{t("a product can be edited if its informations change, it should not be recreated for each contracts", {capfirst: true})}</Text>
<Blockquote
mt="md"
icon={<IconInfoCircle/>}
>
<Text>{t("to add a use the", {capfirst: true, section: t("a product")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("products")})}</Text>
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
</Blockquote>
</Stack>
<Stack gap="1">
<Title order={4}>{t("a form", {capfirst: true})}</Title>
<Text>
{t("create your contract form, it will create a form in the home page (accessible to users)", {capfirst: true})}
<ActionIcon
ml="4"
size="xs"
component={Link}
to="/dashboard/products"
aria-label={t("link to the section", {capfirst: true, section: t("forms")})}
style={{cursor: "pointer", alignSelf: "center"}}
><IconLink/></ActionIcon>
</Text>
<Text>{t("a new contract form should be created for each new season, do not edit a previous contract and change it's values (for history purpose)", {capfirst: true})}</Text>
<Blockquote
mt="md"
icon={<IconInfoCircle/>}
>
<Text>{t("to add a use the", {capfirst: true, section: t("a form")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("forms")})}</Text>
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
</Blockquote>
</Stack>
<Stack gap="1">
<Title order={4}>{t("the shipments", {capfirst: true})}</Title>
<Text>
{t("create shipments for your contract form", {capfirst: true})}
<ActionIcon
ml="4"
size="xs"
component={Link}
to="/dashboard/products"
aria-label={t("link to the section", {capfirst: true, section: t("shipments")})}
style={{cursor: "pointer", alignSelf: "center"}}
><IconLink/></ActionIcon>
</Text>
<Text>
{t("all shipments should be recreated for each form creation", {capfirst: true})}
</Text>
<Blockquote
mt="md"
icon={<IconInfoCircle/>}
>
<Text>{t("to add a use the", {capfirst: true, section: t("a shipment")})} <ActionIcon size="sm"><IconPlus/></ActionIcon> {t("button in top left of the page", {section: t("shipments")})}</Text>
<Text>{t("to edit a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm"><IconEdit/></ActionIcon> {t("button in front of the line you want to edit")}</Text>
<Text>{t("to delete a use the", {capfirst: true, section: t("a productor")})} <ActionIcon size="sm" color="red"><IconX/></ActionIcon> {t("button in front of the line you want to delete")}</Text>
</Blockquote>
</Stack>
<Title order={3}>{t("glossary", {capfirst: true})}</Title>
<Stack>
<Title order={4} fw={700}>{t("product type", {capfirst: true})}</Title>
<Text>{t("a product type define the way it will be organized on the final contract form (showed to users) it can be reccurent or occassional. Recurrent products will be set for all shipments if selected by user, Occasional products can be choosen for each shipments", {capfirst: true})}</Text>
<Stack>
<Text>{t("example in user forms", {capfirst: true})} ({t("recurrent product")}) :</Text>
<Blockquote
color="black"
icon={<IconTestPipe/>}
aria-label={t("link to the section", {
capfirst: true,
section: t("productors"),
})}
style={{ cursor: "pointer", alignSelf: "center" }}
>
<Title order={5}>{t('recurrent products')}</Title>
<IconLink />
</ActionIcon>
</Text>
<Text>
{t(
"a productor can be edited if its informations change, it should not be recreated for each contracts",
{ capfirst: true },
)}
</Text>
<Blockquote mt="md" icon={<IconInfoCircle />}>
<Text>
{t("to add a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm">
<IconPlus />
</ActionIcon>{" "}
{t("button in top left of the page", { section: t("productors") })}
</Text>
<Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm">
<IconEdit />
</ActionIcon>{" "}
{t("button in front of the line you want to edit")}
</Text>
<Text>
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm" color="red">
<IconX />
</ActionIcon>{" "}
{t("button in front of the line you want to delete")}
</Text>
</Blockquote>
</Stack>
<Stack gap="1">
<Title order={4}>{t("the products", { capfirst: true })}</Title>
<Text>
{t("add all products linked to this productor in the products section", {
capfirst: true,
})}
<ActionIcon
ml="4"
size="xs"
component={Link}
to="/dashboard/products"
aria-label={t("link to the section", {
capfirst: true,
section: t("products"),
})}
style={{ cursor: "pointer", alignSelf: "center" }}
>
<IconLink />
</ActionIcon>
</Text>
<Text>
{t(
"a product can be edited if its informations change, it should not be recreated for each contracts",
{ capfirst: true },
)}
</Text>
<Blockquote mt="md" icon={<IconInfoCircle />}>
<Text>
{t("to add a use the", { capfirst: true, section: t("a product") })}{" "}
<ActionIcon size="sm">
<IconPlus />
</ActionIcon>{" "}
{t("button in top left of the page", { section: t("products") })}
</Text>
<Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm">
<IconEdit />
</ActionIcon>{" "}
{t("button in front of the line you want to edit")}
</Text>
<Text>
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm" color="red">
<IconX />
</ActionIcon>{" "}
{t("button in front of the line you want to delete")}
</Text>
</Blockquote>
</Stack>
<Stack gap="1">
<Title order={4}>{t("a form", { capfirst: true })}</Title>
<Text>
{t(
"create your contract form, it will create a form in the home page (accessible to users)",
{ capfirst: true },
)}
<ActionIcon
ml="4"
size="xs"
component={Link}
to="/dashboard/products"
aria-label={t("link to the section", {
capfirst: true,
section: t("forms"),
})}
style={{ cursor: "pointer", alignSelf: "center" }}
>
<IconLink />
</ActionIcon>
</Text>
<Text>
{t(
"a new contract form should be created for each new season, do not edit a previous contract and change it's values (for history purpose)",
{ capfirst: true },
)}
</Text>
<Blockquote mt="md" icon={<IconInfoCircle />}>
<Text>
{t("to add a use the", { capfirst: true, section: t("a form") })}{" "}
<ActionIcon size="sm">
<IconPlus />
</ActionIcon>{" "}
{t("button in top left of the page", { section: t("forms") })}
</Text>
<Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm">
<IconEdit />
</ActionIcon>{" "}
{t("button in front of the line you want to edit")}
</Text>
<Text>
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm" color="red">
<IconX />
</ActionIcon>{" "}
{t("button in front of the line you want to delete")}
</Text>
</Blockquote>
</Stack>
<Stack gap="1">
<Title order={4}>{t("the shipments", { capfirst: true })}</Title>
<Text>
{t("create shipments for your contract form", { capfirst: true })}
<ActionIcon
ml="4"
size="xs"
component={Link}
to="/dashboard/products"
aria-label={t("link to the section", {
capfirst: true,
section: t("shipments"),
})}
style={{ cursor: "pointer", alignSelf: "center" }}
>
<IconLink />
</ActionIcon>
</Text>
<Text>
{t("all shipments should be recreated for each form creation", {
capfirst: true,
})}
</Text>
<Blockquote mt="md" icon={<IconInfoCircle />}>
<Text>
{t("to add a use the", { capfirst: true, section: t("a shipment") })}{" "}
<ActionIcon size="sm">
<IconPlus />
</ActionIcon>{" "}
{t("button in top left of the page", { section: t("shipments") })}
</Text>
<Text>
{t("to edit a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm">
<IconEdit />
</ActionIcon>{" "}
{t("button in front of the line you want to edit")}
</Text>
<Text>
{t("to delete a use the", { capfirst: true, section: t("a productor") })}{" "}
<ActionIcon size="sm" color="red">
<IconX />
</ActionIcon>{" "}
{t("button in front of the line you want to delete")}
</Text>
</Blockquote>
</Stack>
<Title order={3}>{t("glossary", { capfirst: true })}</Title>
<Stack>
<Title order={4} fw={700}>
{t("product type", { capfirst: true })}
</Title>
<Text>
{t(
"a product type define the way it will be organized on the final contract form (showed to users) it can be reccurent or occassional. Recurrent products will be set for all shipments if selected by user, Occasional products can be choosen for each shipments",
{ capfirst: true },
)}
</Text>
<Stack>
<Text>
{t("example in user forms", { capfirst: true })} ({t("recurrent product")})
:
</Text>
<Blockquote color="black" icon={<IconTestPipe />}>
<Title order={5}>{t("recurrent products", { capfirst: true })}</Title>
<Text size="sm">
{t("your selection in this category will apply for all shipments", {
capfirst: true,
})}
</Text>
<NumberInput
label={t("product example", {capfirst: true})}
label={t("product example", { capfirst: true })}
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
/>
</Blockquote>
<Text>{t("example in user forms", {capfirst: true})} ({t("occasional product")}) :</Text>
<Blockquote
color="black"
icon={<IconTestPipe/>}
>
<Title order={5}>{t('occasional products')}</Title>
<Text>{t("select products per shipment")}</Text>
<Text>
{t("example in user forms", { capfirst: true })} ({t("occasional product")})
:
</Text>
<Blockquote color="black" icon={<IconTestPipe />}>
<Title order={5}>{t("occasional products", { capfirst: true })}</Title>
<Text>{t("select products per shipment", { capfirst: true })}</Text>
<Accordion defaultValue={"0"}>
<Accordion.Item value={"example1"}>
<Accordion.Control>{t("shipment", {capfirst: true})} 1</Accordion.Control>
<Accordion.Control>
{t("shipment", { capfirst: true })} 1
</Accordion.Control>
<Accordion.Panel>
<NumberInput
label={t("product example", {capfirst: true})}
label={t("product example", { capfirst: true })}
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
@@ -161,10 +295,12 @@ export function Help() {
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value={"example2"}>
<Accordion.Control>{t("shipment", {capfirst: true})} 2</Accordion.Control>
<Accordion.Control>
{t("shipment", { capfirst: true })} 2
</Accordion.Control>
<Accordion.Panel>
<NumberInput
label={t("product example", {capfirst: true})}
label={t("product example", { capfirst: true })}
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("piece")}`}
@@ -174,58 +310,84 @@ export function Help() {
</Accordion>
</Blockquote>
</Stack>
<Title order={4} fw={700}>{t("sell unit", {capfirst: true})}</Title>
<Text>{t("the product unit will be assigned to the quantity requested in the form")}</Text>
<Title order={4} fw={700}>
{t("sell unit", { capfirst: true })}
</Title>
<Text>
{t("the product unit will be assigned to the quantity requested in the form")}
</Text>
<Stack w={"100%"}>
<Text>{t("example in user forms", {capfirst: true})} ({t("with grams as product unit selected")}) :</Text>
<Blockquote
color="black"
icon={<IconTestPipe/>}
>
<Text>
{t("example in user forms", { capfirst: true })} (
{t("with grams as product unit selected")}) :
</Text>
<Blockquote color="black" icon={<IconTestPipe />}>
<NumberInput
label={t("product example", {capfirst: true})}
label={t("product example", { capfirst: true })}
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("grams")}`}
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("grams")}`}
/>
</Blockquote>
</Stack>
<Title order={4} fw={700}>{t("payment methods", {capfirst: true})}</Title>
<Text>{t("payment methods are defined for a productor. At the end of a form a section payment method let the user select his prefered payment method", {capfirst: true})}</Text>
<Title order={4} fw={700}>
{t("payment methods", { capfirst: true })}
</Title>
<Text>
{t(
"payment methods are defined for a productor. At the end of a form a section payment method let the user select his prefered payment method",
{ capfirst: true },
)}
</Text>
<Stack>
<Text>{t("example in user forms", {capfirst: true})} ({t("with cheque and transfer")}) :</Text>
<Blockquote
color="black"
icon={<IconTestPipe/>}
>
<Text>
{t("example in user forms", { capfirst: true })} (
{t("with cheque and transfer")}) :
</Text>
<Blockquote color="black" icon={<IconTestPipe />}>
<Title order={5}>{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={[t("cheque", {capfirst: true}), t("transfer", {capfirst: true})]}
data={[
t("cheque", { capfirst: true }),
t("transfer", { capfirst: true }),
]}
/>
</Blockquote>
</Stack>
<Title order={4} fw={700}>{t("product quantity", {capfirst: true})}</Title>
<Text>{t("this field is optionnal a product can have a quantity if configured inside the product it will be shown inside the form", {capfirst: true})}</Text>
<Title order={4} fw={700}>{t("product quantity unit", {capfirst: true})}</Title>
<Text>{t("this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form", {capfirst: true})}</Text>
<Text>{t("example in user forms", {capfirst: true})} ({t("with 150 set as quantity and g as quantity unit in product")}):</Text>
<Blockquote
color="black"
icon={<IconTestPipe/>}
>
<Title order={4} fw={700}>
{t("product quantity", { capfirst: true })}
</Title>
<Text>
{t(
"this field is optionnal a product can have a quantity if configured inside the product it will be shown inside the form",
{ capfirst: true },
)}
</Text>
<Title order={4} fw={700}>
{t("product quantity unit", { capfirst: true })}
</Title>
<Text>
{t(
"this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form",
{ capfirst: true },
)}
</Text>
<Text>
{t("example in user forms", { capfirst: true })} (
{t("with 150 set as quantity and g as quantity unit in product")}):
</Text>
<Blockquote color="black" icon={<IconTestPipe />}>
<NumberInput
label={`${t("product example", {capfirst: true})} 150g`}
label={`${t("product example", { capfirst: true })} 150g`}
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("pieces")}`}
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t("pieces")}`}
/>
</Blockquote>
</Stack>
</Stack>
);
}
}

View File

@@ -0,0 +1,22 @@
import { Loader } from "@mantine/core";
import { useEffect } from "react";
import { useSearchParams } from "react-router";
export function Login() {
const [searchParams] = useSearchParams();
useEffect(() => {
const accessToken = searchParams.get("access_token");
const idToken = searchParams.get("id_token");
const refreshToken = searchParams.get("refresh_token");
if (accessToken && idToken) {
localStorage.setItem("access_token", accessToken);
localStorage.setItem("id_token", idToken);
localStorage.setItem("refresh_token", refreshToken || "");
window.location.href = "/";
}
}, [searchParams]);
return <Loader />;
}

View File

@@ -12,6 +12,8 @@ import { Contract } from "./pages/Contract";
import { NotFound } from "./pages/NotFound";
import Contracts from "./pages/Contracts";
import { Help } from "./pages/Help";
import { Login } from "./pages/Login";
import { Auth } from "./components/Auth";
export const router = createBrowserRouter([
{
@@ -22,29 +24,36 @@ export const router = createBrowserRouter([
{ index: true, Component: Home },
{ path: "/forms", Component: Forms },
{
path: "/dashboard",
Component: Dashboard,
element: <Auth />,
children: [
{ path: "help", Component: Help },
{ path: "productors", Component: Productors },
{ path: "productors/create", Component: Productors },
{ path: "productors/:id/edit", Component: Productors },
{ path: "products", Component: Products },
{ path: "products/create", Component: Products },
{ path: "products/:id/edit", Component: Products },
{ path: "contracts", Component: Contracts },
{ path: "users", Component: Users },
{ path: "users/create", Component: Users },
{ path: "users/:id/edit", Component: Users },
{ path: "forms", Component: Forms },
{ path: "forms/:id/edit", Component: Forms },
{ path: "forms/create", Component: Forms },
{ path: "shipments", Component: Shipments },
{ path: "shipments/:id/edit", Component: Shipments },
{ path: "shipments/create", Component: Shipments },
{
path: "/dashboard",
Component: Dashboard,
children: [
{ path: "help", Component: Help },
{ path: "productors", Component: Productors },
{ path: "productors/create", Component: Productors },
{ path: "productors/:id/edit", Component: Productors },
{ path: "products", Component: Products },
{ path: "products/create", Component: Products },
{ path: "products/:id/edit", Component: Products },
{ path: "contracts", Component: Contracts },
{ path: "contracts/download", Component: Contracts },
{ path: "users", Component: Users },
{ path: "users/create", Component: Users },
{ path: "users/:id/edit", Component: Users },
{ path: "forms", Component: Forms },
{ path: "forms/:id/edit", Component: Forms },
{ path: "forms/create", Component: Forms },
{ path: "shipments", Component: Shipments },
{ path: "shipments/:id/edit", Component: Shipments },
{ path: "shipments/create", Component: Shipments },
],
},
],
},
{ path: "/form/:id", Component: Contract },
{ path: "/auth/login", Component: Login },
],
},
]);

View File

@@ -13,7 +13,13 @@ import type {
ProductorCreate,
ProductorEditPayload,
} from "@/services/resources/productors";
import type { User, UserCreate, UserEditPayload } from "@/services/resources/users";
import type {
Role,
User,
UserCreate,
UserEditPayload,
UserLogged,
} from "@/services/resources/users";
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
import type { Contract, ContractCreate } from "./resources/contracts";
import { notifications } from "@mantine/notifications";
@@ -24,9 +30,9 @@ export function useGetShipments(filters?: URLSearchParams): UseQueryResult<Shipm
return useQuery<Shipment[]>({
queryKey: ["shipments", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`).then(
(res) => res.json(),
),
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
}
@@ -36,7 +42,10 @@ export function useGetShipment(
): UseQueryResult<Shipment, Error> {
return useQuery<Shipment>({
queryKey: ["shipment"],
queryFn: () => fetch(`${Config.backend_uri}/shipments/${id}`).then((res) => res.json()),
queryFn: () =>
fetch(`${Config.backend_uri}/shipments/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
...options,
});
@@ -49,6 +58,7 @@ export function useCreateShipment() {
mutationFn: (newShipment: ShipmentCreate) => {
return fetch(`${Config.backend_uri}/shipments`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -65,7 +75,9 @@ export function useCreateShipment() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the shipment") }),
message:
error?.message ||
t(`error create`, { capfirst: true, entity: t("of the shipment") }),
color: "red",
});
},
@@ -79,6 +91,7 @@ export function useEditShipment() {
mutationFn: ({ shipment, id }: ShipmentEditPayload) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -95,7 +108,9 @@ export function useEditShipment() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the shipment") }),
message:
error?.message ||
t(`error edit`, { capfirst: true, entity: t("of the shipment") }),
color: "red",
});
},
@@ -108,6 +123,7 @@ export function useDeleteShipment() {
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -123,7 +139,9 @@ export function useDeleteShipment() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the contract") }),
message:
error?.message ||
t(`error delete`, { capfirst: true, entity: t("of the contract") }),
color: "red",
});
},
@@ -135,9 +153,9 @@ export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Prod
return useQuery<Productor[]>({
queryKey: ["productors", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`).then(
(res) => res.json(),
),
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
}
@@ -147,7 +165,10 @@ export function useGetProductor(
) {
return useQuery<Productor>({
queryKey: ["productor"],
queryFn: () => fetch(`${Config.backend_uri}/productors/${id}`).then((res) => res.json()),
queryFn: () =>
fetch(`${Config.backend_uri}/productors/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
...options,
});
@@ -160,6 +181,7 @@ export function useCreateProductor() {
mutationFn: (newProductor: ProductorCreate) => {
return fetch(`${Config.backend_uri}/productors`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -176,7 +198,9 @@ export function useCreateProductor() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the productor") }),
message:
error?.message ||
t(`error create`, { capfirst: true, entity: t("of the productor") }),
color: "red",
});
},
@@ -190,6 +214,7 @@ export function useEditProductor() {
mutationFn: ({ productor, id }: ProductorEditPayload) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -206,7 +231,9 @@ export function useEditProductor() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the productor") }),
message:
error?.message ||
t(`error edit`, { capfirst: true, entity: t("of the productor") }),
color: "red",
});
},
@@ -219,6 +246,7 @@ export function useDeleteProductor() {
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -234,7 +262,9 @@ export function useDeleteProductor() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the productor") }),
message:
error?.message ||
t(`error delete`, { capfirst: true, entity: t("of the productor") }),
color: "red",
});
},
@@ -247,7 +277,10 @@ export function useGetForm(
) {
return useQuery<Form>({
queryKey: ["form"],
queryFn: () => fetch(`${Config.backend_uri}/forms/${id}`).then((res) => res.json()),
queryFn: () =>
fetch(`${Config.backend_uri}/forms/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
...options,
});
@@ -258,9 +291,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}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
}
@@ -271,6 +304,7 @@ export function useCreateForm() {
mutationFn: (newForm: FormCreate) => {
return fetch(`${Config.backend_uri}/forms`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -289,6 +323,7 @@ export function useDeleteForm() {
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/forms/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -304,7 +339,9 @@ export function useDeleteForm() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the form") }),
message:
error?.message ||
t(`error delete`, { capfirst: true, entity: t("of the form") }),
color: "red",
});
},
@@ -318,6 +355,7 @@ export function useEditForm() {
mutationFn: ({ id, form }: FormEditPayload) => {
return fetch(`${Config.backend_uri}/forms/${id}`, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -334,7 +372,8 @@ export function useEditForm() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the form") }),
message:
error?.message || t(`error edit`, { capfirst: true, entity: t("of the form") }),
color: "red",
});
},
@@ -347,7 +386,10 @@ export function useGetProduct(
) {
return useQuery<Product>({
queryKey: ["product"],
queryFn: () => fetch(`${Config.backend_uri}/products/${id}`).then((res) => res.json()),
queryFn: () =>
fetch(`${Config.backend_uri}/products/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
...options,
});
@@ -358,9 +400,9 @@ export function useGetProducts(filters?: URLSearchParams): UseQueryResult<Produc
return useQuery<Product[]>({
queryKey: ["products", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`).then((res) =>
res.json(),
),
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
}
@@ -371,6 +413,7 @@ export function useCreateProduct() {
mutationFn: (newProduct: ProductCreate) => {
return fetch(`${Config.backend_uri}/products`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -387,7 +430,9 @@ export function useCreateProduct() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the productor") }),
message:
error?.message ||
t(`error create`, { capfirst: true, entity: t("of the productor") }),
color: "red",
});
},
@@ -400,6 +445,7 @@ export function useDeleteProduct() {
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -415,7 +461,9 @@ export function useDeleteProduct() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the product") }),
message:
error?.message ||
t(`error delete`, { capfirst: true, entity: t("of the product") }),
color: "red",
});
},
@@ -429,6 +477,7 @@ export function useEditProduct() {
mutationFn: ({ id, product }: ProductEditPayload) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -445,7 +494,9 @@ export function useEditProduct() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the product") }),
message:
error?.message ||
t(`error edit`, { capfirst: true, entity: t("of the product") }),
color: "red",
});
},
@@ -458,7 +509,10 @@ export function useGetUser(
) {
return useQuery<User>({
queryKey: ["user"],
queryFn: () => fetch(`${Config.backend_uri}/users/${id}`).then((res) => res.json()),
queryFn: () =>
fetch(`${Config.backend_uri}/users/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
...options,
});
@@ -469,9 +523,9 @@ export function useGetUsers(filters?: URLSearchParams): UseQueryResult<User[], E
return useQuery<User[]>({
queryKey: ["users", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`).then((res) =>
res.json(),
),
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
}
@@ -482,6 +536,7 @@ export function useCreateUser() {
mutationFn: (newUser: UserCreate) => {
return fetch(`${Config.backend_uri}/users`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -498,7 +553,9 @@ export function useCreateUser() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error create`, { capfirst: true, entity: t("of the user") }),
message:
error?.message ||
t(`error create`, { capfirst: true, entity: t("of the user") }),
color: "red",
});
},
@@ -511,6 +568,7 @@ export function useDeleteUser() {
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/users/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -526,7 +584,9 @@ export function useDeleteUser() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the user") }),
message:
error?.message ||
t(`error delete`, { capfirst: true, entity: t("of the user") }),
color: "red",
});
},
@@ -540,6 +600,7 @@ export function useEditUser() {
mutationFn: ({ id, user }: UserEditPayload) => {
return fetch(`${Config.backend_uri}/users/${id}`, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -556,7 +617,8 @@ export function useEditUser() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error edit`, { capfirst: true, entity: t("of the user") }),
message:
error?.message || t(`error edit`, { capfirst: true, entity: t("of the user") }),
color: "red",
});
},
@@ -568,9 +630,9 @@ export function useGetContracts(filters?: URLSearchParams): UseQueryResult<Contr
return useQuery<Contract[]>({
queryKey: ["contracts", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`).then(
(res) => res.json(),
),
fetch(`${Config.backend_uri}/contracts${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
}
@@ -580,28 +642,68 @@ export function useGetContract(
) {
return useQuery<Contract>({
queryKey: ["contract"],
queryFn: () => fetch(`${Config.backend_uri}/contracts/${id}`).then((res) => res.json()),
queryFn: () =>
fetch(`${Config.backend_uri}/contracts/${id}`, {
credentials: "include",
}).then((res) => res.json()),
enabled: !!id,
...options,
});
}
export function useGetContractFile() {
return useMutation({
mutationFn: async (id: number) => {
const res = await fetch(`${Config.backend_uri}/contracts/${id}/file`, {
credentials: "include",
}).then((res) => res);
export function useGetContractFile(
id?: number,
) {
return useQuery({
queryKey: ["contract"],
queryFn: () => fetch(`${Config.backend_uri}/contracts/${id}/file`)
.then(async (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("filname=")[1].replace(/"/g, "") :
`contract_${id}.pdf`
return {blob, filename};
}),
enabled: !!id,
const filename =
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 useGetAllContractFile() {
return useMutation({
mutationFn: async (form_id: number) => {
const res = await fetch(`${Config.backend_uri}/contracts/${form_id}/files`, {
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_${form_id}.zip`;
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);
},
});
}
@@ -612,6 +714,7 @@ export function useCreateContract() {
mutationFn: (newContract: ContractCreate) => {
return fetch(`${Config.backend_uri}/contracts`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -636,6 +739,7 @@ export function useDeleteContract() {
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/contracts/${id}`, {
method: "DELETE",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
@@ -651,9 +755,54 @@ export function useDeleteContract() {
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error delete`, { capfirst: true, entity: t("of the contract") }),
message:
error?.message ||
t(`error delete`, { capfirst: true, entity: t("of the contract") }),
color: "red",
});
},
});
}
export function useGetRoles(filters?: URLSearchParams): UseQueryResult<Role[], Error> {
const queryString = filters?.toString();
return useQuery<Role[]>({
queryKey: ["roles", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/users/roles${filters ? `?${queryString}` : ""}`, {
credentials: "include",
}).then((res) => res.json()),
});
}
export function useCurrentUser() {
return useQuery<UserLogged>({
queryKey: ["currentUser"],
queryFn: () => {
return fetch(`${Config.backend_uri}/auth/user/me`, {
credentials: "include",
}).then((res) => res.json());
},
retry: false,
refetchOnWindowFocus: false,
});
}
export function useLogoutUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => {
return fetch(`${Config.backend_uri}/auth/logout`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["currentUser"] });
},
});
}

View File

@@ -1,15 +1,27 @@
import type { Product } from "@/services/resources/products";
export type UserLogged = {
logged: boolean;
user?: User;
};
export type Role = {
id: number;
name: string;
};
export type User = {
id: number;
name: string;
email: string;
products: Product[];
roles: Role[];
};
export type UserInputs = {
email: string;
name: string;
role_names: string[];
};
export type UserCreate = {