[WIP] add styles

This commit is contained in:
Julien Aldon
2026-03-03 17:58:33 +01:00
125 changed files with 5762 additions and 622 deletions

20
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM node:20.19-alpine AS build
WORKDIR /app
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
COPY frontend/package.json frontend/package-lock.json /app/
RUN npm install
COPY frontend/ .
RUN npm run build
FROM nginx:latest
COPY --from=build /app/dist /srv/www/frontend
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=build /app/nginx/default.conf /etc/nginx/conf.d/default.conf

12
frontend/Dockerfile.dev Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20 AS dev
WORKDIR /app
COPY frontend/package.json ./
COPY frontend/package-lock.json ./
RUN npm install
COPY frontend .
CMD ["npm", "run", "dev", "--", "--host"]

View File

@@ -75,12 +75,17 @@
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "some contracts require a minimum value per shipment. Ignore this field if it does not apply to your contract.",
"export contracts": "export contracts",
"download recap": "download recap",
"fill contract online": "fill contract online",
"download base template to print": "download base template to print",
"to export contracts submissions before sending to the productor go to the contracts section": "to export contracts submissions before sending to the productor go to the contracts section.",
"in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract": "in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract",
"you can download all contracts for your form using the export all": "you can download all contracts for your form using the export all",
"in the same corner you can download a recap by clicking on the button": "in the same corner you can download a recap by clicking on the",
"once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page": "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page",
"by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form",
"contracts": "contracts",
"hidden": "hidden",
"visible": "visible",
"minimum price for this shipment should be at least": "minimum price for this shipment should be at least",
"there is": "there is",
"for this contract": "for this contract.",
@@ -90,6 +95,8 @@
"templates": "templates",
"users": "users",
"forms": "contract forms",
"max cheque number": "max cheque number",
"can be empty default to 3": "can be empty default to 3",
"form": "contract form",
"select a form": "select a form",
"download contracts": "download contracts",
@@ -199,6 +206,7 @@
"your session has expired please log in again": "your session has expired please log in again",
"session expired": "session expired",
"user not allowed": "user not allowed",
"roles": "roles",
"your keycloak user has no roles, please contact your administrator": "your keycloak user has no roles, please contact your administrator",
"choose payment method": "choose your payment method (you do not need to pay now).",
"the product unit will be assigned to the quantity requested in the form": "the product unit defines the unit used in the contract form.",

View File

@@ -73,7 +73,10 @@
"shipment products is necessary only for occasional products (if all products are recurrent leave empty)": "il est nécessaire de configurer les produits pour la livraison uniquement si il y a des produits occasionnels (laisser vide si tous les produits sont récurents).",
"recurrent product is for all shipments, occasional product is for a specific shipment (see shipment form)": "les produits récurrents sont pour toutes les livraisons, les produits occasionnels sont pour une livraison particulière (voir formulaire de création de livraison).",
"some contracts require a minimum value per shipment, ignore this field if it's not the case": "certains contrats nécessitent une valeur minimum par livraison. Ce champ peut être ignoré sil ne sapplique pas à votre contrat.",
"by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "en cochant cette option le formulaire sera accessible publiquement sur la page d'accueil, cochez cette option uniquement si tout est prêt avec votre formulaire.",
"contracts": "contrats",
"hidden": "caché",
"visible": "visible",
"minimum price for this shipment should be at least": "le prix minimum d'une livraison doit être au moins de",
"there is": "il y a",
"for this contract": "pour ce contrat.",
@@ -89,9 +92,13 @@
"all contracts": "tous les contrats",
"remove contract": "supprimer le contrat",
"download contract": "télécharger le contrat",
"fill contract online": "remplir le contrat en ligne",
"download base template to print": "télécharger le contrat à remplir sur papier",
"by selecting a form here you can download all contracts of your form": "en selectionnant un formulaire, vous téléchargez tous les contrats pour un formulaire donné.",
"edit user": "modifier l'utilisateur·trice",
"remove user": "supprimer l'utilisateur·trice",
"max cheque number": "numbre maximum de cheques possible",
"can be empty default to 3": "optionnel, la valeur par défaut est à 3 cheques",
"all forms": "tous les formulaires de contrat",
"create new form": "créer un nouveau formulaire de contrat",
"actions": "actions",
@@ -162,7 +169,7 @@
"with cheque and transfer": "avec chèques et virements configuré pour le producteur",
"mililiter": "mililitres (ml)",
"this field is optionnal a product can have a quantity if configured inside the product it will be shown inside the form": "ce champ est optionnel dans la configuration d'un produit, il représente la quantité d'un produit (poids d'une tranche de foie, poids d'un panier, taille d'un bocal...). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens.",
"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": "ce champs est optionnel dans la configuation d'un produit, il représente l'unité de mesure associé à la quantité d'un produit (g, kg, ml, L). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens à coté de la quantité du produit.",
"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": "ce champs est optionnel dans la configuation d'un produit, il représente l'unité de mesure associée à la quantité d'un produit (g, kg, ml, L). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens à coté de la quantité du produit.",
"with 150 set as quantity and g as quantity unit in product": "avec \"150\" en quantité de produit et \"grammes\" selectionné dans l'unité de quantité du produit",
"all shipments should be recreated for each form creation": "les livraisons étant liées à un formulaire elles doivent être recréés pour chaque nouveau formulaire.",
"a productor can be edited if its informations change, it should not be recreated for each contracts": "un(e) producteur·trice peut être édité si ses informations changent, il/elle ne doit pas être recréé pour chaque nouveau contrat.",
@@ -199,6 +206,7 @@
"your session has expired please log in again": "votre session a expiré veuillez vous reconnecter.",
"session expired": "session expirée",
"user not allowed": "utilisateur non authorisé",
"roles": "roles",
"your keycloak user has no roles, please contact your administrator": "votre utilisateur keycloak n'a pas de roles configurés, contactez votre administrateur.",
"choose payment method": "choisissez votre méthode de paiement (vous n'avez pas à payer tout de suite, uniquement renseigner comment vous souhaitez régler votre commande).",
"the product unit will be assigned to the quantity requested in the form": "l'unité de vente du produit définit l'unité associée à la quantité demandée dans le formulaire des amapiens.",

View File

@@ -0,0 +1,22 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
root /srv/www/frontend;
index index.html;
location /api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://backend:8000;
}
location / {
try_files $uri /index.html;
}
}

View File

@@ -1,17 +1,53 @@
import { Badge, Box, Group, Paper, Text, Title } from "@mantine/core";
import { ActionIcon, Badge, Box, Group, Paper, Text, Title, Tooltip } from "@mantine/core";
import { Link } from "react-router";
import type { Form } from "@/services/resources/forms";
import { IconDownload, IconExternalLink } from "@tabler/icons-react";
import { t } from "@/config/i18n";
import { useGetContractFileTemplate } from "@/services/api";
export type FormCardProps = {
form: Form;
};
export function FormCard({ form }: FormCardProps) {
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>
</Group>
<Box
component={Link}
to={`/form/${form.id}`}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none", color: "black" }}
>
<Group justify="space-between" wrap="nowrap">

View File

@@ -1,5 +1,6 @@
import {
Button,
Checkbox,
Group,
Modal,
NumberInput,
@@ -33,6 +34,7 @@ export default function FormModal({ opened, onClose, currentForm, handleSubmit }
productor_id: currentForm?.productor?.id.toString() ?? "",
referer_id: currentForm?.referer?.id.toString() ?? "",
minimum_shipment_value: currentForm?.minimum_shipment_value ?? null,
visible: currentForm?.visible ?? false
},
validate: {
name: (value) =>
@@ -136,6 +138,11 @@ export default function FormModal({ opened, onClose, currentForm, handleSubmit }
radius="sm"
{...form.getInputProps("minimum_shipment_value")}
/>
<Checkbox mt="lg"
label={t("visible", {capfirst: true})}
description={t("by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form", {capfirst: true})}
{...form.getInputProps("visible", {type: "checkbox"})}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"

View File

@@ -1,4 +1,4 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { ActionIcon, Badge, Table, Tooltip } from "@mantine/core";
import { useNavigate, useSearchParams } from "react-router";
import { useDeleteForm } from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react";
@@ -16,6 +16,12 @@ export default function FormRow({ form }: FormRowProps) {
return (
<Table.Tr key={form.id}>
<Table.Td>
{form.visible ?
<Badge color="green">{t("visible", {capfirst: true})}</Badge> :
<Badge color="red">{t("hidden", {capfirst: true})}</Badge>
}
</Table.Td>
<Table.Td>{form.name}</Table.Td>
<Table.Td>{form.season}</Table.Td>
<Table.Td>{form.start}</Table.Td>

View File

@@ -29,13 +29,13 @@ export function Navbar() {
) : null}
</Group>
{!user?.logged ? (
<NavLink
<a
href={`${Config.backend_uri}/auth/login`}
className={"navLink"}
aria-label={t("login with keycloak", { capfirst: true })}
to={`${Config.backend_uri}/auth/login`}
>
{t("login with keycloak", { capfirst: true })}
</NavLink>
</a>
) : (
<a
href={`${Config.backend_uri}/auth/logout`}

View File

@@ -1,13 +1,14 @@
import { t } from "@/config/i18n";
import type { ContractInputs } from "@/services/resources/contracts";
import { Group, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core";
import type { Productor } from "@/services/resources/productors";
import { Group, NumberInput, Stack, TextInput, Title } from "@mantine/core";
import type { UseFormReturnType } from "@mantine/form";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
export type ContractChequeProps = {
inputForm: UseFormReturnType<ContractInputs>;
price: number;
chequeOrder: string;
productor: Productor;
};
export type Cheque = {
@@ -15,7 +16,7 @@ export type Cheque = {
value: string;
};
export function ContractCheque({ inputForm, price, chequeOrder }: ContractChequeProps) {
export function ContractCheque({ inputForm, price, productor }: ContractChequeProps) {
useEffect(() => {
if (!inputForm.values.payment_method.includes("cheque")) {
return;
@@ -41,9 +42,13 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputForm.values.cheque_quantity, price, inputForm.values.cheques]);
const paymentMethod = useMemo(() => {
return productor?.payment_methods.find((el) => el.name === "cheque")
}, [productor]);
return (
<Stack>
<Title order={4}>{`${t("order name")} : ${chequeOrder}`}</Title>
<Title order={4}>{`${t("order name")} : ${paymentMethod?.details}`}</Title>
<NumberInput
label={t("cheque quantity", { capfirst: true })}
placeholder={t("enter cheque quantity", { capfirst: true })}
@@ -52,7 +57,7 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque
{ capfirst: true },
)}
min={1}
max={3}
max={paymentMethod?.max && paymentMethod?.max !== "" ? Number(paymentMethod?.max) : 3}
{...inputForm.getInputProps(`cheque_quantity`)}
/>
<Group grow>
@@ -64,7 +69,7 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque
{...inputForm.getInputProps(`cheques.${index}.name`)}
error={
cheque.name == "" ?
<Text size="sm" c="red">{inputForm?.errors.cheques}</Text> :
inputForm?.errors.cheques :
null
}
/>

View File

@@ -3,7 +3,9 @@ import {
Group,
Modal,
MultiSelect,
NumberInput,
Select,
Stack,
TextInput,
Title,
type ModalBaseProps,
@@ -107,6 +109,7 @@ export function ProductorModal({
existing ?? {
name,
details: "",
max: "",
}
);
}),
@@ -115,12 +118,19 @@ export function ProductorModal({
/>
{form.values.payment_methods.map((method, index) =>
method.name === "cheque" ? (
<TextInput
key={index}
label={t("order name", { capfirst: true })}
placeholder={t("order name", { capfirst: true })}
{...form.getInputProps(`payment_methods.${index}.details`)}
/>
<Stack key={index}>
<TextInput
label={t("order name", { capfirst: true })}
placeholder={t("order name", { capfirst: true })}
{...form.getInputProps(`payment_methods.${index}.details`)}
/>
<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})}
{...form.getInputProps(`payment_methods.${index}.max`)}
/>
</Stack>
) : null,
)}
<Group mt="sm" justify="space-between">

View File

@@ -36,7 +36,7 @@ export function ProductModal({ opened, onClose, currentProduct, handleSubmit }:
quantity: currentProduct?.quantity ?? null,
quantity_unit: currentProduct?.quantity_unit ?? null,
type: currentProduct?.type ?? null,
productor_id: currentProduct ? String(currentProduct.productor.id) : null,
productor_id: currentProduct ? String(currentProduct?.productor?.id) : null,
},
validate: {
name: (value) =>

View File

@@ -13,7 +13,7 @@ import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
import { useForm } from "@mantine/form";
import { useMemo } from "react";
import { type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
import { useGetForms, useGetProductors, useGetProducts } from "@/services/api";
import { useGetReferentForms, useGetProductors, useGetProducts } from "@/services/api";
export type ShipmentModalProps = ModalBaseProps & {
currentShipment?: Shipment;
@@ -43,7 +43,7 @@ export default function ShipmentModal({
},
});
const { data: allForms } = useGetForms();
const { data: allForms } = useGetReferentForms();
const { data: allProducts } = useGetProducts(new URLSearchParams("types=1"));
const { data: allProductors } = useGetProductors();

View File

@@ -3,7 +3,6 @@ import {
Group,
Modal,
MultiSelect,
Select,
TextInput,
Title,
type ModalBaseProps,

View File

@@ -1,4 +1,4 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { ActionIcon, Badge, Box, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react";
import { type User } from "@/services/resources/users";
@@ -18,6 +18,31 @@ export default function UserRow({ user }: UserRowProps) {
<Table.Tr key={user.id}>
<Table.Td>{user.name}</Table.Td>
<Table.Td>{user.email}</Table.Td>
<Table.Td style={{maxWidth: 200}}>
<Box
style={{
display: 'flex',
gap: 4
}}
>
{user.roles.slice(0, 3).map((value) => (
<Badge key={value.id} size="xs">
{t(value.name, { capfirst: true })}
</Badge>
))}
{
user.roles.length > 3 && (
<Tooltip
label={user.roles.slice(3).map(role=>`${role.name} `)}
>
<Badge size="xs" variant="light">
+{user.roles.length - 3}
</Badge>
</Tooltip>
)
}
</Box>
</Table.Td>
<Table.Td>
<Tooltip label={t("edit user", { capfirst: true })}>
<ActionIcon

View File

@@ -18,7 +18,7 @@ import {
Title,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
import { IconDownload, IconMail, IconPhone, IconUser } from "@tabler/icons-react";
import { useCallback, useMemo, useRef } from "react";
import { useParams } from "react-router";
import { computePrices } from "./price";
@@ -35,7 +35,7 @@ export function Contract() {
email: "",
phone: "",
payment_method: "",
cheque_quantity: 0,
cheque_quantity: 1,
cheques: [],
products: {},
},
@@ -134,12 +134,15 @@ export function Contract() {
return;
}
if (inputForm.isValid() && isShipmentsMinimumValue()) {
const formValues = inputForm.getValues();
const contract = {
...inputForm.getValues(),
...formValues,
cheque_quantity: formValues.payment_method === "cheque" ? formValues.cheque_quantity : 0,
form_id: form.id,
products: tranformProducts(withDefaultValues(inputForm.getValues().products)),
products: tranformProducts(withDefaultValues(formValues.products)),
};
await createContractMutation.mutateAsync(contract);
window.location.href = '/';
} else {
const firstErrorField = Object.keys(errors.errors)[0];
const ref = inputRefs.current[firstErrorField];
@@ -283,10 +286,7 @@ export function Contract() {
/>
{inputForm.values.payment_method === "cheque" ? (
<ContractCheque
chequeOrder={
form?.productor?.payment_methods.find((el) => el.name === "cheque")
?.details || ""
}
productor={form?.productor}
price={price}
inputForm={inputForm}
/>
@@ -316,8 +316,10 @@ export function Contract() {
currency: "EUR",
}).format(price)}
</Text>
<Button aria-label={t("submit contract")} onClick={handleSubmit}>
{t("submit contract")}
<Button
leftSection={<IconDownload/>}
aria-label={t("submit contracts")} onClick={handleSubmit}>
{t("submit contract", {capfirst: true})}
</Button>
</Overlay>
</Stack>

View File

@@ -89,6 +89,7 @@ export default function Contracts() {
label={t("download recap", { capfirst: true })}
>
<ActionIcon
disabled={false}
onClick={(e) => {
e.stopPropagation();
navigate(

View File

@@ -1,5 +1,5 @@
import { Stack, Loader, Title, Group, ActionIcon, Tooltip, Table, ScrollArea } from "@mantine/core";
import { useCreateForm, useEditForm, useGetForm, useGetForms } from "@/services/api";
import { useCreateForm, useEditForm, useGetForm, useGetReferentForms } from "@/services/api";
import { t } from "@/config/i18n";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { IconPlus } from "@tabler/icons-react";
@@ -28,12 +28,12 @@ export function Forms() {
navigate(`/dashboard/forms${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const { isPending, data } = useGetForms(searchParams);
const { isPending, data } = useGetReferentForms(searchParams);
const { data: currentForm } = useGetForm(Number(editId), {
enabled: !!editId,
});
const { data: allForms } = useGetForms();
const { data: allForms } = useGetReferentForms();
const seasons = useMemo(() => {
return allForms
@@ -148,6 +148,7 @@ export function Forms() {
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("visible", { capfirst: true })}</Table.Th>
<Table.Th>{t("name", { capfirst: true })}</Table.Th>
<Table.Th>{t("type", { capfirst: true })}</Table.Th>
<Table.Th>{t("start", { capfirst: true })}</Table.Th>

View File

@@ -58,6 +58,11 @@ export default function Productors() {
async (productor: ProductorInputs) => {
await createProductorMutation.mutateAsync({
...productor,
payment_methods: productor.payment_methods.map((payment) =>( {
name: payment.name,
details: payment.details,
max: payment.max === "" ? null : payment.max
}))
});
closeModal();
},
@@ -69,7 +74,14 @@ export default function Productors() {
if (!id) return;
await editProductorMutation.mutateAsync({
id: id,
productor: productor,
productor: {
...productor,
payment_methods: productor.payment_methods.map((payment) =>( {
name: payment.name,
details: payment.details,
max: payment.max === "" ? null : payment.max
}))
},
});
closeModal();
},
@@ -146,7 +158,7 @@ export default function Productors() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{productors.map((productor) => (
{productors?.map((productor) => (
<ProductorRow productor={productor} key={productor.id} />
))}
</Table.Tbody>

View File

@@ -127,11 +127,12 @@ export default function Users() {
<Table.Tr>
<Table.Th>{t("name", { capfirst: true })}</Table.Th>
<Table.Th>{t("email", { capfirst: true })}</Table.Th>
<Table.Th>{t("roles", { capfirst: true })}</Table.Th>
<Table.Th>{t("actions", { capfirst: true })}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{users.map((user) => (
{users?.map((user) => (
<UserRow user={user} key={user.id} />
))}
</Table.Tbody>

View File

@@ -29,7 +29,7 @@ export async function refreshToken() {
return await fetch(`${Config.backend_uri}/auth/refresh`, {method: "POST", credentials: "include"});
}
export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) {
export async function fetchWithAuth(input: RequestInfo, options?: RequestInit, redirect: boolean = true) {
const res = await fetch(input, {
credentials: "include",
...options,
@@ -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 = `/?sessionExpired=True`;
if (redirect)
window.location.href = `/?sessionExpired=True`;
const error = new Error("Unauthorized");
error.cause = 401
@@ -329,6 +330,15 @@ export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], E
});
}
export function useGetReferentForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> {
const queryString = filters?.toString();
return useQuery<Form[]>({
queryKey: ["forms", queryString],
queryFn: () =>
fetchWithAuth(`${Config.backend_uri}/forms/referents${filters ? `?${queryString}` : ""}`).then((res) => res.json()),
});
}
export function useCreateForm() {
const queryClient = useQueryClient();
@@ -710,6 +720,33 @@ 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);
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}.pdf`;
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) => {
@@ -751,7 +788,6 @@ export function useGetAllContractFile() {
disposition && disposition?.includes("filename=")
? disposition.split("filename=")[1].replace(/"/g, "")
: `contract_${form_id}.zip`;
console.log(disposition);
return { blob, filename };
},
onSuccess: ({ blob, filename }) => {
@@ -769,20 +805,28 @@ export function useCreateContract() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newContract: ContractCreate) => {
return fetch(`${Config.backend_uri}/contracts`, {
mutationFn: async (newContract: ContractCreate) => {
const res = await fetch(`${Config.backend_uri}/contracts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newContract),
}).then(async (res) => await res.blob());
}).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_${newContract.form_id}.pdf`;
return { blob, filename };
},
onSuccess: async (pdfBlob) => {
const url = URL.createObjectURL(pdfBlob);
onSuccess: async ({ blob, filename }) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `contract.pdf`;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
await queryClient.invalidateQueries({ queryKey: ["contracts"] });
@@ -836,9 +880,9 @@ export function useCurrentUser() {
return useQuery<UserLogged>({
queryKey: ["currentUser"],
queryFn: () => {
return fetch(`${Config.backend_uri}/auth/user/me`, {
return fetchWithAuth(`${Config.backend_uri}/auth/user/me`, {
credentials: "include",
}).then((res) => res.json());
}, false).then((res) => res.json());
},
retry: false,
});

View File

@@ -12,6 +12,7 @@ export type Form = {
referer: User;
shipments: Shipment[];
minimum_shipment_value: number | null;
visible: boolean;
};
export type FormCreate = {
@@ -22,6 +23,7 @@ export type FormCreate = {
productor_id: number;
referer_id: number;
minimum_shipment_value: number | null;
visible: boolean;
};
export type FormEdit = {
@@ -32,6 +34,7 @@ export type FormEdit = {
productor_id?: number | null;
referer_id?: number | null;
minimum_shipment_value: number | null;
visible: boolean;
};
export type FormEditPayload = {
@@ -47,4 +50,5 @@ export type FormInputs = {
productor_id: string;
referer_id: string;
minimum_shipment_value: number | string | null;
visible: boolean;
};

View File

@@ -9,6 +9,7 @@ export const PaymentMethods = [
export type PaymentMethod = {
name: string;
details: string;
max: number | string | null;
};
export type Productor = {

View File

@@ -15,7 +15,7 @@ export type User = {
name: string;
email: string;
products: Product[];
roles: string[];
roles: Role[];
};
export type UserInputs = {