[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

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 = {