add prettier code formater

This commit is contained in:
2026-02-15 11:32:30 +01:00
parent 11b3a926d2
commit 627ddfc464
61 changed files with 7471 additions and 7312 deletions

View File

@@ -1,51 +1,47 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
project: './tsconfig.json', // Required for type-aware rules
project: "./tsconfig.json", // Required for type-aware rules
},
env: {
browser: true,
es2021: true,
node: true,
},
plugins: [
'react',
'react-hooks',
'@typescript-eslint',
'jsx-a11y',
'import',
'unused-imports',
],
plugins: ["react", "react-hooks", "@typescript-eslint", "jsx-a11y", "import", "unused-imports"],
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'prettier',
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:jsx-a11y/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"prettier",
],
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: false }],
'@typescript-eslint/strict-boolean-expressions': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/typedef': [
'error',
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: false }],
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/prefer-readonly": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/typedef": [
"error",
{
arrayDestructuring: true,
arrowParameter: true,
@@ -58,46 +54,49 @@ module.exports = {
},
],
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/jsx-uses-react': 'off',
'react/jsx-uses-vars': 'error',
'react/jsx-no-useless-fragment': 'error',
'react/self-closing-comp': 'error',
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/jsx-uses-react": "off",
"react/jsx-uses-vars": "error",
"react/jsx-no-useless-fragment": "error",
"react/self-closing-comp": "error",
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
'jsx-a11y/no-noninteractive-element-interactions': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
"jsx-a11y/no-noninteractive-element-interactions": "error",
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/click-events-have-key-events": "error",
'import/order': [
'error',
"import/order": [
"error",
{
groups: [['builtin', 'external'], ['internal', 'parent', 'sibling', 'index']],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
groups: [
["builtin", "external"],
["internal", "parent", "sibling", "index"],
],
"newlines-between": "always",
alphabetize: { order: "asc", caseInsensitive: true },
},
],
'import/no-unresolved': 'error',
'import/no-duplicates': 'error',
"import/no-unresolved": "error",
"import/no-duplicates": "error",
'unused-imports/no-unused-imports-ts': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'eqeqeq': ['error', 'always'],
'curly': 'error',
'semi': ['error', 'always'],
'quotes': ['error', 'single', { avoidEscape: true }],
'prefer-const': 'error',
'no-var': 'error',
"unused-imports/no-unused-imports-ts": "error",
"no-console": ["warn", { allow: ["warn", "error"] }],
"no-debugger": "error",
eqeqeq: ["error", "always"],
curly: "error",
semi: ["error", "always"],
quotes: ["error", "single", { avoidEscape: true }],
"prefer-const": "error",
"no-var": "error",
},
settings: {
react: {
version: 'detect',
version: "detect",
},
'import/resolver': {
"import/resolver": {
typescript: {},
},
},

8
frontend/.prettierignore Normal file
View File

@@ -0,0 +1,8 @@
node_modules
dist
build
coverage
.next
out
public
*.lock

13
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,13 @@
{
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}

View File

@@ -17,9 +17,9 @@ If you are developing a production application, we recommend updating the config
```js
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
@@ -34,40 +34,40 @@ export default defineConfig([
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
]);
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
reactX.configs["recommended-typescript"],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
]);
```

View File

@@ -1,14 +1,14 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
@@ -20,4 +20,4 @@ export default defineConfig([
globals: globals.browser,
},
},
])
]);

View File

@@ -41,6 +41,7 @@
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.8.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
@@ -3332,6 +3333,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",

View File

@@ -7,6 +7,7 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier . --write",
"preview": "vite preview"
},
"dependencies": {
@@ -43,6 +44,7 @@
"postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.8.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"

View File

@@ -1,13 +1,13 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},

View File

@@ -1,7 +1,3 @@
export function Footer() {
return (
<footer>
</footer>
);
return <footer></footer>;
}

View File

@@ -4,26 +4,18 @@ import type { Form } from "@/services/resources/forms";
export type FormCardProps = {
form: Form;
}
};
export function FormCard({ form }: FormCardProps) {
return (
<Paper
shadow="xl"
p="xl"
miw={{base: "100vw", md: "25vw", lg:"20vw"}}
>
<Paper shadow="xl" p="xl" miw={{ base: "100vw", md: "25vw", lg: "20vw" }}>
<Box
component={Link}
to={`/form/${form.id}`}
style={{ textDecoration: "none", color: "black" }}
>
<Group justify="space-between" wrap="nowrap">
<Title
order={3}
textWrap="wrap"
lineClamp={1}
>
<Title order={3} textWrap="wrap" lineClamp={1}>
{form.name}
</Title>
<Badge>{form.season}</Badge>
@@ -34,6 +26,5 @@ export function FormCard({form}: FormCardProps) {
</Group>
</Box>
</Paper>
);
}

View File

@@ -7,19 +7,19 @@ export type FilterFormsProps = {
productors: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
};
export default function FilterForms({
seasons,
productors,
filters,
onFilterChange
onFilterChange,
}: FilterFormsProps) {
const defaultProductors = useMemo(() => {
return filters.getAll("productors")
return filters.getAll("productors");
}, [filters]);
const defaultSeasons = useMemo(() => {
return filters.getAll("seasons")
return filters.getAll("seasons");
}, [filters]);
return (
@@ -30,7 +30,7 @@ export default function FilterForms({
data={seasons}
defaultValue={defaultSeasons}
onChange={(values: string[]) => {
onFilterChange(values, 'seasons')
onFilterChange(values, "seasons");
}}
clearable
/>
@@ -40,7 +40,7 @@ export default function FilterForms({
data={productors}
defaultValue={defaultProductors}
onChange={(values: string[]) => {
onFilterChange(values, 'productors')
onFilterChange(values, "productors");
}}
clearable
/>

View File

@@ -1,4 +1,12 @@
import { Button, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } from "@mantine/core";
import {
Button,
Group,
Modal,
NumberInput,
Select,
TextInput,
type ModalBaseProps,
} from "@mantine/core";
import { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates";
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
@@ -10,14 +18,9 @@ import type { Form, FormInputs } from "@/services/resources/forms";
export type FormModalProps = ModalBaseProps & {
currentForm?: Form;
handleSubmit: (form: FormInputs, id?: number) => void;
}
};
export default function FormModal({
opened,
onClose,
currentForm,
handleSubmit
}: FormModalProps) {
export default function FormModal({ opened, onClose, currentForm, handleSubmit }: FormModalProps) {
const { data: productors } = useGetProductors();
const { data: users } = useGetUsers();
@@ -33,40 +36,50 @@ export default function FormModal({
},
validate: {
name: (value) =>
!value ? `${t("a name", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("a name", { capfirst: true })} ${t("is required")}` : null,
season: (value) =>
!value ? `${t("a season", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("a season", { capfirst: true })} ${t("is required")}` : null,
start: (value) =>
!value ? `${t("a start date", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("a start date", { capfirst: true })} ${t("is required")}` : null,
end: (value) =>
!value ? `${t("a end date", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("a end date", { capfirst: true })} ${t("is required")}` : null,
productor_id: (value) =>
!value ? `${t("a productor", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("a productor", { capfirst: true })} ${t("is required")}` : null,
referer_id: (value) =>
!value ? `${t("a referer", {capfirst: true})} ${t('is required')}` : null
}
!value ? `${t("a referer", { capfirst: true })} ${t("is required")}` : null,
},
});
const usersSelect = useMemo(() => {
return users?.map(user => ({value: String(user.id), label: `${user.name}`}))
return users?.map((user) => ({
value: String(user.id),
label: `${user.name}`,
}));
}, [users]);
const productorsSelect = useMemo(() => {
return productors?.map(prod => ({value: String(prod.id), label: `${prod.name}`}))
return productors?.map((prod) => ({
value: String(prod.id),
label: `${prod.name}`,
}));
}, [productors]);
return (
<Modal
opened={opened}
onClose={onClose}
title={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
title={
currentForm
? t("edit form", { capfirst: true })
: t("create form", { capfirst: true })
}
>
<TextInput
label={t("form name", { capfirst: true })}
placeholder={t("form name", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
{...form.getInputProps("name")}
/>
<TextInput
label={t("contract season", { capfirst: true })}
@@ -74,20 +87,20 @@ export default function FormModal({
description={t("contract season recommandation", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps('season')}
{...form.getInputProps("season")}
/>
<Group grow>
<DatePickerInput
label={t("start date", { capfirst: true })}
placeholder={t("start date", { capfirst: true })}
withAsterisk
{...form.getInputProps('start')}
{...form.getInputProps("start")}
/>
<DatePickerInput
label={t("end date", { capfirst: true })}
placeholder={t("end date", { capfirst: true })}
withAsterisk
{...form.getInputProps('end')}
{...form.getInputProps("end")}
/>
</Group>
<Select
@@ -99,7 +112,7 @@ export default function FormModal({
allowDeselect
searchable
data={usersSelect || []}
{...form.getInputProps('referer_id')}
{...form.getInputProps("referer_id")}
/>
<Select
label={t("productor", { capfirst: true })}
@@ -110,14 +123,17 @@ export default function FormModal({
allowDeselect
searchable
data={productorsSelect || []}
{...form.getInputProps('productor_id')}
{...form.getInputProps("productor_id")}
/>
<NumberInput
label={t("minimum shipment value", { capfirst: true })}
placeholder={t("minimum shipment value", { capfirst: true })}
description={t("some contracts require a minimum value per shipment, ignore this field if it's not the case", {capfirst: true})}
description={t(
"some contracts require a minimum value per shipment, ignore this field if it's not the case",
{ capfirst: true },
)}
radius="sm"
{...form.getInputProps('minimum_shipment_value')}
{...form.getInputProps("minimum_shipment_value")}
/>
<Group mt="sm" justify="space-between">
<Button
@@ -129,18 +145,28 @@ export default function FormModal({
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
>
{t("cancel", { capfirst: true })}
</Button>
<Button
variant="filled"
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
aria-label={
currentForm
? t("edit form", { capfirst: true })
: t("create form", { capfirst: true })
}
leftSection={currentForm ? <IconEdit /> : <IconPlus />}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentForm?.id)
handleSubmit(form.getValues(), currentForm?.id);
}
}}
>{currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}</Button>
>
{currentForm
? t("edit form", { capfirst: true })
: t("create form", { capfirst: true })}
</Button>
</Group>
</Modal>
);

View File

@@ -7,11 +7,9 @@ import type { Form } from "@/services/resources/forms";
export type FormRowProps = {
form: Form;
}
};
export default function FormRow({
form,
}: FormRowProps) {
export default function FormRow({ form }: FormRowProps) {
const [searchParams] = useSearchParams();
const deleteMutation = useDeleteForm();
const navigate = useNavigate();
@@ -31,7 +29,9 @@ export default function FormRow({
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/forms/${form.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/forms/${form.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconEdit />

View File

@@ -5,11 +5,11 @@ export type InputLabelProps = {
label: string;
info: string;
isRequired?: boolean;
}
};
export function InputLabel({ label, info, isRequired }: InputLabelProps) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<Tooltip label={info}>
<ActionIcon variant="transparent" size="xs" color="gray">
<IconInfoCircle size={16} />
@@ -17,12 +17,8 @@ export function InputLabel({label, info, isRequired}: InputLabelProps) {
</Tooltip>
<span>
{label}
{
isRequired ?
<span style={{ color: 'red' }}> *</span> : null
}
{isRequired ? <span style={{ color: "red" }}> *</span> : null}
</span>
</div>
);
}

View File

@@ -8,16 +8,12 @@ export function Navbar() {
return (
<nav>
<Group>
<NavLink
className={"navLink"}
aria-label={t('home')}
to="/"
>
<NavLink className={"navLink"} aria-label={t("home")} to="/">
{t("home", { capfirst: true })}
</NavLink>
<NavLink
className={"navLink"}
aria-label={t('dashboard')}
aria-label={t("dashboard")}
to="/dashboard/productors"
>
{t("dashboard", { capfirst: true })}

View File

@@ -7,19 +7,19 @@ export type ProductorsFiltersProps = {
types: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
};
export default function ProductorsFilter({
names,
types,
filters,
onFilterChange
onFilterChange,
}: ProductorsFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("names")
return filters.getAll("names");
}, [filters]);
const defaultTypes = useMemo(() => {
return filters.getAll("types")
return filters.getAll("types");
}, [filters]);
return (
@@ -30,7 +30,7 @@ export default function ProductorsFilter({
data={names}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, 'names')
onFilterChange(values, "names");
}}
clearable
searchable
@@ -41,7 +41,7 @@ export default function ProductorsFilter({
data={types}
defaultValue={defaultTypes}
onChange={(values: string[]) => {
onFilterChange(values, 'types')
onFilterChange(values, "types");
}}
clearable
searchable

View File

@@ -1,19 +1,31 @@
import { Button, Group, Modal, MultiSelect, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import {
Button,
Group,
Modal,
MultiSelect,
TextInput,
Title,
type ModalBaseProps,
} from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react";
import { PaymentMethods, type Productor, type ProductorInputs } from "@/services/resources/productors";
import {
PaymentMethods,
type Productor,
type ProductorInputs,
} from "@/services/resources/productors";
export type ProductorModalProps = ModalBaseProps & {
currentProductor?: Productor;
handleSubmit: (productor: ProductorInputs, id?: number) => void;
}
};
export function ProductorModal({
opened,
onClose,
currentProductor,
handleSubmit
handleSubmit,
}: ProductorModalProps) {
const form = useForm<ProductorInputs>({
initialValues: {
@@ -28,16 +40,12 @@ export function ProductorModal({
address: (value) =>
!value ? `${t("address", { capfirst: true })} ${t("is required")}` : null,
type: (value) =>
!value ? `${t("type", {capfirst: true})} ${t("is required")}` : null
}
!value ? `${t("type", { capfirst: true })} ${t("is required")}` : null,
},
});
return (
<Modal
opened={opened}
onClose={onClose}
title={t("create productor", {capfirst: true})}
>
<Modal opened={opened} onClose={onClose} title={t("create productor", { capfirst: true })}>
<Title order={4}>{t("Informations", { capfirst: true })}</Title>
<TextInput
label={t("productor name", { capfirst: true })}
@@ -68,41 +76,44 @@ export function ProductorModal({
data={PaymentMethods}
clearable
searchable
value={form.values.payment_methods.map(p => p.name)}
value={form.values.payment_methods.map((p) => p.name)}
onChange={(names) => {
form.setFieldValue("payment_methods", names.map(name => {
const existing = form.values.payment_methods.find(p => p.name === name);
return existing ?? {
form.setFieldValue(
"payment_methods",
names.map((name) => {
const existing = form.values.payment_methods.find(
(p) => p.name === name,
);
return (
existing ?? {
name,
details: ""
};
}));
details: "",
}
);
}),
);
}}
/>
{
form.values.payment_methods.map((method, index) => (
{form.values.payment_methods.map((method, index) => (
<TextInput
key={index}
label={
method.name === "cheque" ?
t("order name", {capfirst: true}) :
method.name === "transfer" ?
t("IBAN") :
t("details", {capfirst: true})
method.name === "cheque"
? t("order name", { capfirst: true })
: method.name === "transfer"
? t("IBAN")
: t("details", { capfirst: true })
}
placeholder={
method.name === "cheque" ?
t("order name", {capfirst: true}) :
method.name === "transfer" ?
t("IBAN") :
t("details", {capfirst: true})
method.name === "cheque"
? t("order name", { capfirst: true })
: method.name === "transfer"
? t("IBAN")
: t("details", { capfirst: true })
}
{...form.getInputProps(
`payment_methods.${index}.details`
)}
{...form.getInputProps(`payment_methods.${index}.details`)}
/>
))
}
))}
<Group mt="sm" justify="space-between">
<Button
variant="filled"
@@ -113,17 +124,27 @@ export function ProductorModal({
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
>
{t("cancel", { capfirst: true })}
</Button>
<Button
variant="filled"
aria-label={currentProductor ? t("edit productor", {capfirst: true}) : t("create productor", {capfirst: true})}
aria-label={
currentProductor
? t("edit productor", { capfirst: true })
: t("create productor", { capfirst: true })
}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentProductor?.id)
handleSubmit(form.getValues(), currentProductor?.id);
}
}}
>{currentProductor ? t("edit productor", {capfirst: true}) : t("create productor", {capfirst: true})}</Button>
>
{currentProductor
? t("edit productor", { capfirst: true })
: t("create productor", { capfirst: true })}
</Button>
</Group>
</Modal>
);

View File

@@ -7,11 +7,9 @@ import { useNavigate, useSearchParams } from "react-router";
export type ProductorRowProps = {
productor: Productor;
}
};
export default function ProductorRow({
productor,
}: ProductorRowProps) {
export default function ProductorRow({ productor }: ProductorRowProps) {
const [searchParams] = useSearchParams();
const deleteMutation = useDeleteProductor();
const navigate = useNavigate();
@@ -22,13 +20,11 @@ export default function ProductorRow({
<Table.Td>{productor.type}</Table.Td>
<Table.Td>{productor.address}</Table.Td>
<Table.Td>
{
productor.payment_methods.map((value) =>(
{productor.payment_methods.map((value) => (
<Badge key={value.name} ml="xs">
{t(value.name, { capfirst: true })}
</Badge>
))
}
))}
</Table.Td>
<Table.Td>
<Tooltip label={t("edit productor", { capfirst: true })}>
@@ -37,7 +33,9 @@ export default function ProductorRow({
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/productors/${productor.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/productors/${productor.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconEdit />
@@ -57,6 +55,5 @@ export default function ProductorRow({
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -7,19 +7,19 @@ export type ProductsFiltersProps = {
productors: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
};
export default function ProductsFilters({
names,
productors,
filters,
onFilterChange
onFilterChange,
}: ProductsFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("names")
return filters.getAll("names");
}, [filters]);
const defaultProductors = useMemo(() => {
return filters.getAll("productors")
return filters.getAll("productors");
}, [filters]);
return (
@@ -30,7 +30,7 @@ export default function ProductsFilters({
data={names}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, 'names')
onFilterChange(values, "names");
}}
clearable
searchable
@@ -41,7 +41,7 @@ export default function ProductsFilters({
data={productors}
defaultValue={defaultProductors}
onChange={(values: string[]) => {
onFilterChange(values, 'productors')
onFilterChange(values, "productors");
}}
clearable
searchable

View File

@@ -8,36 +8,33 @@ export type ProductFormProps = {
inputForm: UseFormReturnType<Record<string, string | number>>;
product: Product;
shipment?: Shipment;
}
};
export function ProductForm({
inputForm,
product,
shipment,
}: ProductFormProps) {
export function ProductForm({ inputForm, product, shipment }: ProductFormProps) {
return (
<Group mb="sm" grow>
<NumberInput
label={
`${product.name}
label={`${product.name}
${product.quantity || ""}${product.quantity ? product.quantity_unit : ""}
${
product.price ?
Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(product.price) :
product.price_kg && Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(product.price_kg)
product.price
? Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(product.price)
: product.price_kg &&
Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(product.price_kg)
}
${product.price ? `/ ${t(ProductUnit[product.unit])}` : "/ kg"}`
}
description={`${t("enter quantity", {capfirst: true})} ${t('in')} ${t(ProductUnit[product.unit])}`}
${product.price ? `/ ${t(ProductUnit[product.unit])}` : "/ kg"}`}
description={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t(ProductUnit[product.unit])}`}
aria-label={t("enter quantity")}
placeholder={`${t("enter quantity", {capfirst: true})} ${t('in')} ${t(ProductUnit[product.unit])}`}
{...inputForm.getInputProps(shipment ? `planned-${shipment.id}-${product.id}` : `recurrent-${product.id}`)}
placeholder={`${t("enter quantity", { capfirst: true })} ${t("in")} ${t(ProductUnit[product.unit])}`}
{...inputForm.getInputProps(
shipment ? `planned-${shipment.id}-${product.id}` : `recurrent-${product.id}`,
)}
/>
</Group>
);

View File

@@ -1,8 +1,22 @@
import { Button, Group, Modal, NumberInput, Select, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import {
Button,
Group,
Modal,
NumberInput,
Select,
TextInput,
Title,
type ModalBaseProps,
} from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react";
import { ProductQuantityUnit, ProductUnit, type Product, type ProductInputs } from "@/services/resources/products";
import {
ProductQuantityUnit,
ProductUnit,
type Product,
type ProductInputs,
} from "@/services/resources/products";
import { useMemo } from "react";
import { useGetProductors } from "@/services/api";
import { InputLabel } from "@/components/Label";
@@ -10,14 +24,9 @@ import { InputLabel } from "@/components/Label";
export type ProductModalProps = ModalBaseProps & {
currentProduct?: Product;
handleSubmit: (product: ProductInputs, id?: number) => void;
}
};
export function ProductModal({
opened,
onClose,
currentProduct,
handleSubmit
}: ProductModalProps) {
export function ProductModal({ opened, onClose, currentProduct, handleSubmit }: ProductModalProps) {
const { data: productors } = useGetProductors();
const form = useForm<ProductInputs>({
initialValues: {
@@ -32,30 +41,33 @@ export function ProductModal({
},
validate: {
name: (value) =>
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("name", { capfirst: true })} ${t("is required")}` : null,
unit: (value) =>
!value ? `${t("unit", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("unit", { capfirst: true })} ${t("is required")}` : null,
price: (value, values) =>
!value && !values.price_kg ? `${t("price or price_kg", {capfirst: true})} ${t('is required')}` : null,
!value && !values.price_kg
? `${t("price or price_kg", { capfirst: true })} ${t("is required")}`
: null,
price_kg: (value, values) =>
!value && !values.price ? `${t("price or price_kg", {capfirst: true})} ${t('is required')}` : null,
!value && !values.price
? `${t("price or price_kg", { capfirst: true })} ${t("is required")}`
: null,
type: (value) =>
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("type", { capfirst: true })} ${t("is required")}` : null,
productor_id: (value) =>
!value ? `${t("productor", {capfirst: true})} ${t('is required')}` : null
!value ? `${t("productor", { capfirst: true })} ${t("is required")}` : null,
},
});
const productorsSelect = useMemo(() => {
return productors?.map(productor => ({value: String(productor.id), label: `${productor.name}`}))
return productors?.map((productor) => ({
value: String(productor.id),
label: `${productor.name}`,
}));
}, [productors]);
return (
<Modal
opened={opened}
onClose={onClose}
title={t("create product", {capfirst: true})}
>
<Modal opened={opened} onClose={onClose} title={t("create product", { capfirst: true })}>
<Title order={4}>{t("informations", { capfirst: true })}</Title>
<Select
label={t("productor", { capfirst: true })}
@@ -65,20 +77,23 @@ export function ProductModal({
clearable
searchable
data={productorsSelect || []}
{...form.getInputProps('productor_id')}
{...form.getInputProps("productor_id")}
/>
<Group grow>
<TextInput
label={t("product name", { capfirst: true })}
placeholder={t("product name", { capfirst: true })}
radius="sm"
{...form.getInputProps('name')}
{...form.getInputProps("name")}
/>
<Select
label={
<InputLabel
label={t("product type", { capfirst: true })}
info={t("recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)", {capfirst: true})}
info={t(
"recurrent product is for all shipments, planned product is for a specific shipment (see shipment form)",
{ capfirst: true },
)}
isRequired
/>
}
@@ -88,34 +103,40 @@ export function ProductModal({
clearable
data={[
{ value: "1", label: t("planned", { capfirst: true }) },
{value: "2", label: t("recurrent", {capfirst: true})}
{ value: "2", label: t("recurrent", { capfirst: true }) },
]}
{...form.getInputProps('type')}
{...form.getInputProps("type")}
/>
</Group>
<Select
label={t("product unit", { capfirst: true })}
placeholder={t("product unit", { capfirst: true })}
description={t("the product unit will be assigned to the quantity requested in the form", { capfirst: true})}
description={t(
"the product unit will be assigned to the quantity requested in the form",
{ capfirst: true },
)}
radius="sm"
withAsterisk
searchable
clearable
data={Object.entries(ProductUnit).map(([key, value]) => ({value: key, label: t(value, {capfirst: true})}))}
{...form.getInputProps('unit')}
data={Object.entries(ProductUnit).map(([key, value]) => ({
value: key,
label: t(value, { capfirst: true }),
}))}
{...form.getInputProps("unit")}
/>
<Group grow>
<NumberInput
label={t("product price", { capfirst: true })}
placeholder={t("product price", { capfirst: true })}
radius="sm"
{...form.getInputProps('price')}
{...form.getInputProps("price")}
/>
<NumberInput
label={t("product price kg", { capfirst: true })}
placeholder={t("product price kg", { capfirst: true })}
radius="sm"
{...form.getInputProps('price_kg')}
{...form.getInputProps("price_kg")}
/>
</Group>
<Group grow>
@@ -123,7 +144,7 @@ export function ProductModal({
label={t("product quantity", { capfirst: true })}
placeholder={t("product quantity", { capfirst: true })}
radius="sm"
{...form.getInputProps('quantity', {capfirst: true})}
{...form.getInputProps("quantity", { capfirst: true })}
/>
<Select
label={t("product quantity unit", { capfirst: true })}
@@ -131,10 +152,12 @@ export function ProductModal({
radius="sm"
clearable
searchable
data={Object.entries(ProductQuantityUnit).map(([key, value]) => ({value: key, label: t(value, {capfirst: true})}))}
{...form.getInputProps('quantity_unit', {capfirst: true})}
data={Object.entries(ProductQuantityUnit).map(([key, value]) => ({
value: key,
label: t(value, { capfirst: true }),
}))}
{...form.getInputProps("quantity_unit", { capfirst: true })}
/>
</Group>
<Group mt="sm" justify="space-between">
<Button
@@ -146,17 +169,27 @@ export function ProductModal({
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
>
{t("cancel", { capfirst: true })}
</Button>
<Button
variant="filled"
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
aria-label={
currentProduct
? t("edit product", { capfirst: true })
: t("create product", { capfirst: true })
}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentProduct?.id)
handleSubmit(form.getValues(), currentProduct?.id);
}
}}
>{currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}</Button>
>
{currentProduct
? t("edit product", { capfirst: true })
: t("create product", { capfirst: true })}
</Button>
</Group>
</Modal>
);

View File

@@ -7,11 +7,9 @@ import { useNavigate, useSearchParams } from "react-router";
export type ProductRowProps = {
product: Product;
}
};
export default function ProductRow({
product,
}: ProductRowProps) {
export default function ProductRow({ product }: ProductRowProps) {
const [searchParams] = useSearchParams();
const deleteMutation = useDeleteProduct();
const navigate = useNavigate();
@@ -21,25 +19,25 @@ export default function ProductRow({
<Table.Td>{product.name}</Table.Td>
<Table.Td>{t(ProductType[product.type])}</Table.Td>
<Table.Td>
{
product.price ?
Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(product.price) : null
}
{product.price
? Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(product.price)
: null}
</Table.Td>
<Table.Td>
{
product.price_kg ?
`${Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(product.price_kg)}/kg` : null
}
{product.price_kg
? `${Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(product.price_kg)}/kg`
: null}
</Table.Td>
<Table.Td>
{product.quantity}
{product.quantity_unit}
</Table.Td>
<Table.Td>{product.quantity}{product.quantity_unit}</Table.Td>
<Table.Td>{t(ProductUnit[product.unit], { capfirst: true })}</Table.Td>
<Table.Td>
<Tooltip label={t("edit product", { capfirst: true })}>
@@ -48,7 +46,9 @@ export default function ProductRow({
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/products/${product.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/products/${product.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconEdit />
@@ -68,6 +68,5 @@ export default function ProductRow({
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -7,19 +7,19 @@ export type ShipmentFiltersProps = {
forms: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
};
export default function ShipmentsFilters({
names,
forms,
filters,
onFilterChange
onFilterChange,
}: ShipmentFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("names")
return filters.getAll("names");
}, [filters]);
const defaultForms = useMemo(() => {
return filters.getAll("forms")
return filters.getAll("forms");
}, [filters]);
return (
@@ -30,7 +30,7 @@ export default function ShipmentsFilters({
data={names}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, 'names')
onFilterChange(values, "names");
}}
clearable
searchable
@@ -41,7 +41,7 @@ export default function ShipmentsFilters({
data={forms}
defaultValue={defaultForms}
onChange={(values: string[]) => {
onFilterChange(values, 'forms')
onFilterChange(values, "forms");
}}
clearable
searchable

View File

@@ -11,7 +11,7 @@ export type ShipmentFormProps = {
shipment: Shipment;
minimumPrice?: number | null;
index: number;
}
};
export default function ShipmentForm({
shipment,
@@ -20,20 +20,16 @@ export default function ShipmentForm({
minimumPrice,
}: ShipmentFormProps) {
const shipmentPrice = useMemo(() => {
const values = Object
.entries(inputForm.getValues())
.filter(([key]) =>
key.includes("planned") &&
key.split("-")[1] === String(shipment.id)
const values = Object.entries(inputForm.getValues()).filter(
([key]) => key.includes("planned") && key.split("-")[1] === String(shipment.id),
);
return computePrices(values, shipment.products);
}, [inputForm, shipment.products, shipment.id]);
const priceRequirement = useMemo(() => {
if (!minimumPrice)
return false;
return minimumPrice ? shipmentPrice < minimumPrice : true
}, [shipmentPrice, minimumPrice])
if (!minimumPrice) return false;
return minimumPrice ? shipmentPrice < minimumPrice : true;
}, [shipmentPrice, minimumPrice]);
return (
<Accordion.Item value={String(index)}>
@@ -41,43 +37,37 @@ export default function ShipmentForm({
<Group justify="space-between">
<Text>{shipment.name}</Text>
<Stack gap={0}>
<Text c={priceRequirement ? "red" : "green"}>{
Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(shipmentPrice)
}</Text>
{
priceRequirement ?
<Text c={priceRequirement ? "red" : "green"}>
{Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(shipmentPrice)}
</Text>
{priceRequirement ? (
<Text c="red" size="sm">
{`${t("minimum price for this shipment should be at least", { capfirst: true })} ${minimumPrice}`}
</Text> :
null
}
</Text>
) : null}
</Stack>
<Text mr="lg">
{`${
new Date(shipment.date).toLocaleDateString("fr-FR", {
{`${new Date(shipment.date).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
}`}
})}`}
</Text>
</Group>
</Accordion.Control>
<Accordion.Panel>
{
shipment?.products.map((product) => (
{shipment?.products.map((product) => (
<ProductForm
key={product.id}
product={product}
shipment={shipment}
inputForm={inputForm}
/>
))
}
))}
</Accordion.Panel>
</Accordion.Item>
);

View File

@@ -1,4 +1,12 @@
import { Button, Group, Modal, MultiSelect, Select, TextInput, type ModalBaseProps } from "@mantine/core";
import {
Button,
Group,
Modal,
MultiSelect,
Select,
TextInput,
type ModalBaseProps,
} from "@mantine/core";
import { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates";
import { IconCancel } from "@tabler/icons-react";
@@ -10,49 +18,54 @@ import { useGetForms, useGetProductors, useGetProducts } from "@/services/api";
export type ShipmentModalProps = ModalBaseProps & {
currentShipment?: Shipment;
handleSubmit: (shipment: ShipmentInputs, id?: number) => void;
}
};
export default function ShipmentModal({
opened,
onClose,
currentShipment,
handleSubmit
handleSubmit,
}: ShipmentModalProps) {
const form = useForm<ShipmentInputs>({
initialValues: {
name: currentShipment?.name ?? "",
date: currentShipment?.date ?? null,
form_id: currentShipment ? String(currentShipment?.form_id) : "",
product_ids: currentShipment?.products.map((el) => (String(el.id))) ?? []
product_ids: currentShipment?.products.map((el) => String(el.id)) ?? [],
},
validate: {
name: (value) =>
!value ? `${t("a name", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("a name", { capfirst: true })} ${t("is required")}` : null,
date: (value) =>
!value ? `${t("a shipment date", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("a shipment date", { capfirst: true })} ${t("is required")}` : null,
form_id: (value) =>
!value ? `${t("a form", {capfirst: true})} ${t('is required')}` : null,
}
!value ? `${t("a form", { capfirst: true })} ${t("is required")}` : null,
},
});
const { data: allForms } = useGetForms();
const { data: allProducts } = useGetProducts(new URLSearchParams("types=1"));
const { data: allProductors } = useGetProductors()
const { data: allProductors } = useGetProductors();
const formsSelect = useMemo(() => {
return allForms?.map(currentForm => ({value: String(currentForm.id), label: `${currentForm.name} ${currentForm.season}`}))
return allForms?.map((currentForm) => ({
value: String(currentForm.id),
label: `${currentForm.name} ${currentForm.season}`,
}));
}, [allForms]);
const productsSelect = useMemo(() => {
if (!allProducts)
return;
return allProductors?.map(productor => {
if (!allProducts) return;
return allProductors?.map((productor) => {
return {
group: productor.name,
items: allProducts
.filter((product) => product.productor.id === productor.id)
.map((product) => ({value: String(product.id), label: `${product.name}`}))
}
.map((product) => ({
value: String(product.id),
label: `${product.name}`,
})),
};
});
}, [allProductors, allProducts]);
@@ -60,20 +73,20 @@ export default function ShipmentModal({
<Modal
opened={opened}
onClose={onClose}
title={currentShipment ? t("edit shipment") : t('create shipment')}
title={currentShipment ? t("edit shipment") : t("create shipment")}
>
<TextInput
label={t("shipment name", { capfirst: true })}
placeholder={t("shipment name", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
{...form.getInputProps("name")}
/>
<DatePickerInput
label={t("shipment date", { capfirst: true })}
placeholder={t("shipment date", { capfirst: true })}
withAsterisk
{...form.getInputProps('date')}
{...form.getInputProps("date")}
/>
<Select
label={t("shipment form", { capfirst: true })}
@@ -82,16 +95,19 @@ export default function ShipmentModal({
data={formsSelect || []}
clearable
withAsterisk
{...form.getInputProps('form_id')}
{...form.getInputProps("form_id")}
/>
<MultiSelect
label={t("shipment products", { capfirst: true })}
placeholder={t("shipment products", { capfirst: true })}
description={t("shipment products is necessary only for planned products (if all products are recurrent leave empty)", {capfirst: true})}
description={t(
"shipment products is necessary only for planned products (if all products are recurrent leave empty)",
{ capfirst: true },
)}
data={productsSelect || []}
clearable
searchable
{...form.getInputProps('product_ids')}
{...form.getInputProps("product_ids")}
/>
<Group mt="sm" justify="space-between">
<Button
@@ -103,17 +119,27 @@ export default function ShipmentModal({
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
>
{t("cancel", { capfirst: true })}
</Button>
<Button
variant="filled"
aria-label={currentShipment ? t("edit shipment", {capfirst: true}) : t('create shipment', {capfirst: true})}
aria-label={
currentShipment
? t("edit shipment", { capfirst: true })
: t("create shipment", { capfirst: true })
}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentShipment?.id)
handleSubmit(form.getValues(), currentShipment?.id);
}
}}
>{currentShipment ? t("edit shipment", {capfirst: true}) : t('create shipment', {capfirst: true})}</Button>
>
{currentShipment
? t("edit shipment", { capfirst: true })
: t("create shipment", { capfirst: true })}
</Button>
</Group>
</Modal>
);

View File

@@ -7,11 +7,9 @@ import type { Shipment } from "@/services/resources/shipments";
export type ShipmentRowProps = {
shipment: Shipment;
}
};
export default function ShipmentRow({
shipment,
}: ShipmentRowProps) {
export default function ShipmentRow({ shipment }: ShipmentRowProps) {
const [searchParams] = useSearchParams();
const deleteMutation = useDeleteShipment();
const navigate = useNavigate();
@@ -28,7 +26,9 @@ export default function ShipmentRow({
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/shipments/${shipment.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/shipments/${shipment.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconEdit />

View File

@@ -6,15 +6,11 @@ export type UserFiltersProps = {
names: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
};
export default function UserFilters({
names,
filters,
onFilterChange
}: UserFiltersProps) {
export default function UserFilters({ names, filters, onFilterChange }: UserFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("names")
return filters.getAll("names");
}, [filters]);
return (
@@ -25,7 +21,7 @@ export default function UserFilters({
data={names}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, 'names')
onFilterChange(values, "names");
}}
clearable
/>

View File

@@ -7,47 +7,38 @@ import { type User, type UserInputs } from "@/services/resources/users";
export type UserModalProps = ModalBaseProps & {
currentUser?: User;
handleSubmit: (user: UserInputs, id?: number) => void;
}
};
export function UserModal({
opened,
onClose,
currentUser,
handleSubmit
}: UserModalProps) {
export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserModalProps) {
const form = useForm<UserInputs>({
initialValues: {
name: currentUser?.name ?? "",
email: currentUser?.email ?? ""
email: currentUser?.email ?? "",
},
validate: {
name: (value) =>
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
!value ? `${t("name", { capfirst: true })} ${t("is required")}` : null,
email: (value) =>
!value ? `${t("email", {capfirst: true})} ${t('is required')}` : null,
}
!value ? `${t("email", { capfirst: true })} ${t("is required")}` : null,
},
});
return (
<Modal
opened={opened}
onClose={onClose}
title={t("create user", {capfirst: true})}
>
<Modal opened={opened} onClose={onClose} title={t("create user", { capfirst: true })}>
<Title order={4}>{t("informations", { capfirst: true })}</Title>
<TextInput
label={t("user name", { capfirst: true })}
placeholder={t("user name", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
{...form.getInputProps("name")}
/>
<TextInput
label={t("user email", { capfirst: true })}
placeholder={t("user email", { capfirst: true })}
radius="sm"
withAsterisk
{...form.getInputProps('email')}
{...form.getInputProps("email")}
/>
<Group mt="sm" justify="space-between">
<Button
@@ -59,17 +50,27 @@ export function UserModal({
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
>
{t("cancel", { capfirst: true })}
</Button>
<Button
variant="filled"
aria-label={currentUser ? t("edit user", {capfirst: true}) : t('create user', {capfirst: true})}
aria-label={
currentUser
? t("edit user", { capfirst: true })
: t("create user", { capfirst: true })
}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentUser?.id)
handleSubmit(form.getValues(), currentUser?.id);
}
}}
>{currentUser ? t("edit user", {capfirst: true}) : t('create user', {capfirst: true})}</Button>
>
{currentUser
? t("edit user", { capfirst: true })
: t("create user", { capfirst: true })}
</Button>
</Group>
</Modal>
);

View File

@@ -7,11 +7,9 @@ import { useNavigate, useSearchParams } from "react-router";
export type UserRowProps = {
user: User;
}
};
export default function UserRow({
user,
}: UserRowProps) {
export default function UserRow({ user }: UserRowProps) {
const [searchParams] = useSearchParams();
const deleteMutation = useDeleteUser();
const navigate = useNavigate();
@@ -27,7 +25,9 @@ export default function UserRow({
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/users/${user.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/users/${user.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconEdit />
@@ -47,6 +47,5 @@ export default function UserRow({
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -1,4 +1,4 @@
export const Config = {
backend_uri: import.meta.env.VITE_API_URL,
debug: import.meta.env.NODE_ENV === "development"
}
debug: import.meta.env.NODE_ENV === "development",
};

View File

@@ -30,17 +30,13 @@ i18next
.then(() => {
[Settings.defaultLocale] = i18next.language.split("-");
i18next.services.formatter?.add(
"capfirst",
(value) => {
i18next.services.formatter?.add("capfirst", (value) => {
if (typeof value !== "string" || !value.length) {
return value;
}
return value.charAt(0).toUpperCase() + value.slice(1);
}
);
});
});
export function t(message: string, params?: Record<string, unknown>) {
const result = i18next.t(message, params);

View File

@@ -4,12 +4,12 @@ import { RouterProvider } from "react-router";
import { router } from "@/router.tsx";
import { MantineProvider } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/notifications/styles.css";
import { Notifications } from "@mantine/notifications";
const queryClient = new QueryClient()
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<StrictMode>
@@ -19,5 +19,5 @@ createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />
</MantineProvider>
</QueryClientProvider>
</StrictMode>
</StrictMode>,
);

View File

@@ -3,7 +3,18 @@ import ShipmentForm from "@/components/Shipments/Form";
import { t } from "@/config/i18n";
import { useCreateContract, useGetForm } from "@/services/api";
import { type Product } from "@/services/resources/products";
import { Accordion, Button, Group, List, Loader, Overlay, Stack, Text, TextInput, Title } from "@mantine/core";
import {
Accordion,
Button,
Group,
List,
Loader,
Overlay,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
import { useCallback, useMemo, useRef } from "react";
@@ -21,17 +32,21 @@ export function Contract() {
phone: "",
},
validate: {
firstname: (value) => !value ? `${t("a firstname", {capfirst: true})} ${t("is required")}` : null,
lastname: (value) => !value ? `${t("a lastname", {capfirst: true})} ${t("is required")}` : null,
email: (value) => !value ? `${t("a email", {capfirst: true})} ${t("is required")}` : null,
phone: (value) => !value ? `${t("a phone", {capfirst: true})} ${t("is required")}` : null,
}
firstname: (value) =>
!value ? `${t("a firstname", { capfirst: true })} ${t("is required")}` : null,
lastname: (value) =>
!value ? `${t("a lastname", { capfirst: true })} ${t("is required")}` : null,
email: (value) =>
!value ? `${t("a email", { capfirst: true })} ${t("is required")}` : null,
phone: (value) =>
!value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
},
});
const createContractMutation = useCreateContract();
const productsRecurent = useMemo(() => {
return form?.productor?.products.filter((el) => el.type === "2")
return form?.productor?.products.filter((el) => el.type === "2");
}, [form]);
const shipments = useMemo(() => {
@@ -40,7 +55,7 @@ export function Contract() {
const allProducts = useMemo(() => {
return form?.productor?.products;
}, [form])
}, [form]);
const price = useMemo(() => {
if (!allProducts) {
@@ -54,17 +69,16 @@ export function Contract() {
firstname: null,
lastname: null,
email: null,
phone: null
phone: null,
});
const isShipmentsMinimumValue = useCallback(() => {
if (!form)
return false;
if (!form) return false;
const shipmentErrors = form.shipments
.map((shipment) => {
const total = computePrices(
Object.entries(inputForm.getValues()),
shipment.products
shipment.products,
);
if (total < (form?.minimum_shipment_value || 0)) {
return shipment.id;
@@ -75,9 +89,9 @@ export function Contract() {
return shipmentErrors.length === 0;
}, [form, inputForm]);
const withDefaultValues = useCallback((values: Record<string, number | string>) => {
if (!productsRecurent || !form)
return {};
const withDefaultValues = useCallback(
(values: Record<string, number | string>) => {
if (!productsRecurent || !form) return {};
const result = { ...values };
productsRecurent.forEach((product: Product) => {
@@ -93,11 +107,13 @@ export function Contract() {
if (result[key] === undefined || result[key] === "") {
result[key] = 0;
}
})
});
});
return result;
}, [productsRecurent, form]);
},
[productsRecurent, form],
);
const handleSubmit = useCallback(async () => {
const errors = inputForm.validate();
@@ -108,35 +124,37 @@ export function Contract() {
const contract = {
form_id: form.id,
contract: withDefaultValues(inputForm.getValues()),
}
};
await createContractMutation.mutateAsync(contract);
} else {
const firstErrorField = Object.keys(errors.errors)[0];
const ref = inputRefs.current[firstErrorField];
ref?.scrollIntoView({ behavior: "smooth", block: "center" });
}
}, [inputForm, inputRefs, isShipmentsMinimumValue, form, createContractMutation, withDefaultValues]);
}, [
inputForm,
inputRefs,
isShipmentsMinimumValue,
form,
createContractMutation,
withDefaultValues,
]);
if (!form || !shipments || !productsRecurent)
return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
return (
<Stack w={{ base: "100%", md: "80%", lg: "50%" }}>
<Title order={2}>{form.name}</Title>
<Title order={3}>{t("informations", { capfirst: true })}</Title>
<Text size="sm">
{t("all theses informations are for contract generation", {capfirst: true})}
{t("all theses informations are for contract generation", {
capfirst: true,
})}
</Text>
<Group grow>
<TextInput
@@ -146,8 +164,10 @@ export function Contract() {
withAsterisk
required
leftSection={<IconUser />}
{...inputForm.getInputProps('firstname')}
ref={(el) => {inputRefs.current.firstname = el}}
{...inputForm.getInputProps("firstname")}
ref={(el) => {
inputRefs.current.firstname = el;
}}
/>
<TextInput
label={t("lastname", { capfirst: true })}
@@ -156,8 +176,10 @@ export function Contract() {
withAsterisk
required
leftSection={<IconUser />}
{...inputForm.getInputProps('lastname')}
ref={(el) => {inputRefs.current.lastname = el}}
{...inputForm.getInputProps("lastname")}
ref={(el) => {
inputRefs.current.lastname = el;
}}
/>
</Group>
<Group grow>
@@ -168,8 +190,10 @@ export function Contract() {
withAsterisk
required
leftSection={<IconMail />}
{...inputForm.getInputProps('email')}
ref={(el) => {inputRefs.current.email = el}}
{...inputForm.getInputProps("email")}
ref={(el) => {
inputRefs.current.email = el;
}}
/>
<TextInput
label={t("phone", { capfirst: true })}
@@ -178,53 +202,46 @@ export function Contract() {
withAsterisk
required
leftSection={<IconPhone />}
{...inputForm.getInputProps('phone')}
ref={(el) => {inputRefs.current.phone = el}}
{...inputForm.getInputProps("phone")}
ref={(el) => {
inputRefs.current.phone = el;
}}
/>
</Group>
<Title order={3}>{t('shipments', {capfirst: true})}</Title>
<Title order={3}>{t("shipments", { capfirst: true })}</Title>
<Text>{`${t("there is", { capfirst: true })} ${shipments.length} ${shipments.length > 1 ? t("shipments") : t("shipment")} ${t("for this contract")}`}</Text>
<List>
{
shipments.map(shipment => (
<List.Item key={shipment.id}>{`${shipment.name} :
${
new Date(shipment.date).toLocaleDateString("fr-FR", {
{shipments.map((shipment) => (
<List.Item key={shipment.id}>
{`${shipment.name} :
${new Date(shipment.date).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
}`}
})}`}
</List.Item>
))
}
))}
</List>
{
productsRecurent.length > 0 ?
{productsRecurent.length > 0 ? (
<>
<Title order={3}>{t('recurrent products', {capfirst: true})}</Title>
<Text size="sm">{t('your selection in this category will apply for all shipments', {capfirst: true})}</Text>
{
productsRecurent.map((product) => (
<ProductForm
key={product.id}
product={product}
inputForm={inputForm}
/>
))
}
</> :
null
}
{
shipments.some(shipment => shipment.products.length > 0) ?
<Title order={3}>{t("recurrent products", { capfirst: true })}</Title>
<Text size="sm">
{t("your selection in this category will apply for all shipments", {
capfirst: true,
})}
</Text>
{productsRecurent.map((product) => (
<ProductForm key={product.id} product={product} inputForm={inputForm} />
))}
</>
) : null}
{shipments.some((shipment) => shipment.products.length > 0) ? (
<>
<Title order={3}>{t("planned products")}</Title>
<Text>{t("select products per shipment")}</Text>
<Accordion defaultValue={"0"}>
{
shipments.map((shipment, index) => (
{shipments.map((shipment, index) => (
<ShipmentForm
minimumPrice={form.minimum_shipment_value}
shipment={shipment}
@@ -232,12 +249,10 @@ export function Contract() {
inputForm={inputForm}
key={shipment.id}
/>
))
}
))}
</Accordion>
</> :
null
}
</>
) : null}
<Overlay
bg={"lightGray"}
h="10vh"
@@ -250,19 +265,17 @@ export function Contract() {
bottom: "0px",
}}
>
<Text>{
t("total", {capfirst: true})} : {Intl.NumberFormat(
"fr-FR",
{style: "currency", currency: "EUR"}
).format(price)}
<Text>
{t("total", { capfirst: true })} :{" "}
{Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(price)}
</Text>
<Button
aria-label={t('submit contract')}
onClick={handleSubmit}
>
{t('submit contract')}
<Button aria-label={t("submit contract")} onClick={handleSubmit}>
{t("submit contract")}
</Button>
</Overlay>
</Stack>
)
);
}

View File

@@ -1,6 +1,10 @@
import type { Product } from "@/services/resources/products";
export function computePrices(values: [string, string | number][], products: Product[], nbShipment?: number) {
export function computePrices(
values: [string, string | number][],
products: Product[],
nbShipment?: number,
) {
return values.reduce((prev, [key, value]) => {
const keyArray = key.split("-");
const productId = Number(keyArray[keyArray.length - 1]);
@@ -11,7 +15,9 @@ export function computePrices(values: [string, string | number][], products: Pro
const isRecurent = key.includes("recurrent") && nbShipment;
const productPrice = Number(product.price || product.price_kg);
const productQuantityUnit = product.unit === "2" ? 1 : 1000;
const productQuantity = Number(product.price ? Number(value) : Number(value) / productQuantityUnit);
return(prev + productPrice * productQuantity * (isRecurent ? nbShipment : 1));
const productQuantity = Number(
product.price ? Number(value) : Number(value) / productQuantityUnit,
);
return prev + productPrice * productQuantity * (isRecurent ? nbShipment : 1);
}, 0);
}

View File

@@ -10,7 +10,7 @@ export default function Dashboard() {
<Tabs
w={{ base: "100%", md: "80%", lg: "60%" }}
orientation={"horizontal"}
value={location.pathname.split('/')[2]}
value={location.pathname.split("/")[2]}
defaultValue={"productors"}
onChange={(value) => navigate(`/dashboard/${value}`)}
>

View File

@@ -19,36 +19,40 @@ export function Forms() {
const editId = useMemo(() => {
if (isEdit) {
return location.pathname.split("/")[3]
return location.pathname.split("/")[3];
}
return null
}, [location, isEdit])
return null;
}, [location, isEdit]);
const closeModal = useCallback(() => {
navigate(`/dashboard/forms${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const { isPending, data } = useGetForms(searchParams);
const { data: currentForm } = useGetForm(Number(editId), { enabled: !!editId });
const { data: currentForm } = useGetForm(Number(editId), {
enabled: !!editId,
});
const { data: allForms } = useGetForms();
const seasons = useMemo(() => {
return allForms?.map((form: Form) => (form.season))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allForms])
return allForms
?.map((form: Form) => form.season)
.filter((season, index, array) => array.indexOf(season) === index);
}, [allForms]);
const productors = useMemo(() => {
return allForms?.map((form: Form) => (form.productor.name))
.filter((productor, index, array) => array.indexOf(productor) === index)
}, [allForms])
return allForms
?.map((form: Form) => form.productor.name)
.filter((productor, index, array) => array.indexOf(productor) === index);
}, [allForms]);
const createFormMutation = useCreateForm();
const editFormMutation = useEditForm();
const handleCreateForm = useCallback(async (form: FormInputs) => {
if (!form.start || !form.end)
return;
const handleCreateForm = useCallback(
async (form: FormInputs) => {
if (!form.start || !form.end) return;
await createFormMutation.mutateAsync({
...form,
start: form?.start,
@@ -58,11 +62,13 @@ export function Forms() {
minimum_shipment_value: Number(form.minimum_shipment_value),
});
closeModal();
}, [createFormMutation, closeModal]);
},
[createFormMutation, closeModal],
);
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => {
if (!id)
return;
const handleEditForm = useCallback(
async (form: FormInputs, id?: number) => {
if (!id) return;
await editFormMutation.mutateAsync({
id: id,
form: {
@@ -72,36 +78,32 @@ export function Forms() {
productor_id: Number(form.productor_id),
referer_id: Number(form.referer_id),
minimum_shipment_value: Number(form.minimum_shipment_value),
}
},
});
closeModal();
}, [editFormMutation, closeModal]);
},
[editFormMutation, closeModal],
);
const onFilterChange = useCallback((
values: string[],
filter: string
) => {
setSearchParams(prev => {
const onFilterChange = useCallback(
(values: string[], filter: string) => {
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.delete(filter)
params.delete(filter);
values.forEach(value => {
values.forEach((value) => {
params.append(filter, value);
});
return params;
});
}, [setSearchParams])
},
[setSearchParams],
);
if (!data || isPending)
return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
@@ -114,7 +116,9 @@ export function Forms() {
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/forms/create${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/forms/create${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconPlus />
@@ -154,14 +158,9 @@ export function Forms() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
data.map((form) => (
<FormRow
form={form}
key={form.id}
/>
))
}
{data.map((form) => (
<FormRow form={form} key={form.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>

View File

@@ -9,13 +9,13 @@ export function Home() {
return (
<Flex gap="md" wrap="wrap" justify="center">
{
allForms && allForms?.length > 0 ?
allForms.map((form: Form) => (
<FormCard form={form} key={form.id}/>
)) :
<Text mt="lg" size="lg">{t("there is no contract for now",{capfirst: true})}</Text>
}
{allForms && allForms?.length > 0 ? (
allForms.map((form: Form) => <FormCard form={form} key={form.id} />)
) : (
<Text mt="lg" size="lg">
{t("there is no contract for now", { capfirst: true })}
</Text>
)}
</Flex>
);
}

View File

@@ -4,16 +4,16 @@ import { IconHome } from "@tabler/icons-react";
import { useNavigate } from "react-router";
export function NotFound() {
const navigate = useNavigate()
const navigate = useNavigate();
return (
<Stack justify="center" align="center">
<Title order={2}>{t("oops", { capfirst: true })}</Title>
<Text>{t('this page does not exists', {capfirst: true})}</Text>
<Tooltip label={t('back to home', {capfirst: true})}>
<Text>{t("this page does not exists", { capfirst: true })}</Text>
<Tooltip label={t("back to home", { capfirst: true })}>
<ActionIcon
aria-label={t("back to home", { capfirst: true })}
onClick={() => {
navigate('/')
navigate("/");
}}
>
<IconHome />

View File

@@ -1,6 +1,11 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { useCreateProductor, useEditProductor, useGetProductor, useGetProductors } from "@/services/api";
import {
useCreateProductor,
useEditProductor,
useGetProductor,
useGetProductors,
} from "@/services/api";
import { IconPlus } from "@tabler/icons-react";
import ProductorRow from "@/components/Productors/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
@@ -19,69 +24,76 @@ export default function Productors() {
const editId = useMemo(() => {
if (isEdit) {
return location.pathname.split("/")[3]
return location.pathname.split("/")[3];
}
return null
}, [location, isEdit])
return null;
}, [location, isEdit]);
const closeModal = useCallback(() => {
navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const { data: productors, isPending } = useGetProductors(searchParams);
const { data: currentProductor } = useGetProductor(Number(editId), { enabled: !!editId });
const { data: currentProductor } = useGetProductor(Number(editId), {
enabled: !!editId,
});
const { data: allProductors } = useGetProductors();
const names = useMemo(() => {
return allProductors?.map((productor: Productor) => (productor.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allProductors])
return allProductors
?.map((productor: Productor) => productor.name)
.filter((season, index, array) => array.indexOf(season) === index);
}, [allProductors]);
const types = useMemo(() => {
return allProductors?.map((productor: Productor) => (productor.type))
.filter((productor, index, array) => array.indexOf(productor) === index)
}, [allProductors])
return allProductors
?.map((productor: Productor) => productor.type)
.filter((productor, index, array) => array.indexOf(productor) === index);
}, [allProductors]);
const createProductorMutation = useCreateProductor();
const editProductorMutation = useEditProductor();
const handleCreateProductor = useCallback(async (productor: ProductorInputs) => {
const handleCreateProductor = useCallback(
async (productor: ProductorInputs) => {
await createProductorMutation.mutateAsync({
...productor
...productor,
});
closeModal();
}, [createProductorMutation, closeModal]);
},
[createProductorMutation, closeModal],
);
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => {
if (!id)
return;
const handleEditProductor = useCallback(
async (productor: ProductorInputs, id?: number) => {
if (!id) return;
await editProductorMutation.mutateAsync({
id: id,
productor: productor
productor: productor,
});
closeModal();
}, [editProductorMutation, closeModal]);
},
[editProductorMutation, closeModal],
);
const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => {
const onFilterChange = useCallback(
(values: string[], filter: string) => {
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.delete(filter)
params.delete(filter);
values.forEach(value => {
values.forEach((value) => {
params.append(filter, value);
});
return params;
});
}, [setSearchParams])
},
[setSearchParams],
);
if (!productors || isPending)
return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
@@ -94,7 +106,9 @@ export default function Productors() {
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/productors/create${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/productors/create${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconPlus />
@@ -132,14 +146,9 @@ export default function Productors() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
productors.map((productor) => (
<ProductorRow
productor={productor}
key={productor.id}
/>
))
}
{productors.map((productor) => (
<ProductorRow productor={productor} key={productor.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>

View File

@@ -6,7 +6,11 @@ import ProductRow from "@/components/Products/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { ProductModal } from "@/components/Products/Modal";
import { useCallback, useMemo } from "react";
import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products";
import {
productCreateFromProductInputs,
type Product,
type ProductInputs,
} from "@/services/resources/products";
import ProductsFilters from "@/components/Products/Filter";
export default function Products() {
@@ -19,67 +23,74 @@ export default function Products() {
const editId = useMemo(() => {
if (isEdit) {
return location.pathname.split("/")[3]
return location.pathname.split("/")[3];
}
return null
}, [location, isEdit])
return null;
}, [location, isEdit]);
const closeModal = useCallback(() => {
navigate(`/dashboard/products${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const { data: products, isPending } = useGetProducts(searchParams);
const { data: currentProduct } = useGetProduct(Number(editId), { enabled: !!editId });
const { data: currentProduct } = useGetProduct(Number(editId), {
enabled: !!editId,
});
const { data: allProducts } = useGetProducts();
const names = useMemo(() => {
return allProducts?.map((product: Product) => (product.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allProducts])
return allProducts
?.map((product: Product) => product.name)
.filter((season, index, array) => array.indexOf(season) === index);
}, [allProducts]);
const productors = useMemo(() => {
return allProducts?.map((product: Product) => (product.productor.name))
.filter((productor, index, array) => array.indexOf(productor) === index)
}, [allProducts])
return allProducts
?.map((product: Product) => product.productor.name)
.filter((productor, index, array) => array.indexOf(productor) === index);
}, [allProducts]);
const createProductMutation = useCreateProduct();
const editProductMutation = useEditProduct();
const handleCreateProduct = useCallback(async (product: ProductInputs) => {
const handleCreateProduct = useCallback(
async (product: ProductInputs) => {
await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
closeModal();
}, [createProductMutation, closeModal]);
},
[createProductMutation, closeModal],
);
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => {
if (!id)
return;
const handleEditProduct = useCallback(
async (product: ProductInputs, id?: number) => {
if (!id) return;
await editProductMutation.mutateAsync({
id: id,
product: productCreateFromProductInputs(product)
product: productCreateFromProductInputs(product),
});
closeModal();
}, [editProductMutation, closeModal]);
},
[editProductMutation, closeModal],
);
const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => {
const onFilterChange = useCallback(
(values: string[], filter: string) => {
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.delete(filter);
values.forEach(value => {
values.forEach((value) => {
params.append(filter, value);
});
return params;
});
}, [setSearchParams])
},
[setSearchParams],
);
if (!products || isPending)
return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
@@ -92,7 +103,9 @@ export default function Products() {
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/products/create${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/products/create${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconPlus />
@@ -132,14 +145,9 @@ export default function Products() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
products.map((product) => (
<ProductRow
product={product}
key={product.id}
/>
))
}
{products.map((product) => (
<ProductRow product={product} key={product.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>

View File

@@ -1,11 +1,20 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { useCreateShipment, useEditShipment, useGetShipment, useGetShipments } from "@/services/api";
import {
useCreateShipment,
useEditShipment,
useGetShipment,
useGetShipments,
} from "@/services/api";
import { IconPlus } from "@tabler/icons-react";
import ShipmentRow from "@/components/Shipments/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { useCallback, useMemo } from "react";
import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments";
import {
shipmentCreateFromShipmentInputs,
type Shipment,
type ShipmentInputs,
} from "@/services/resources/shipments";
import ShipmentModal from "@/components/Shipments/Modal";
import ShipmentsFilters from "@/components/Shipments/Filter";
@@ -19,67 +28,74 @@ export default function Shipments() {
const editId = useMemo(() => {
if (isEdit) {
return location.pathname.split("/")[3]
return location.pathname.split("/")[3];
}
return null
}, [location, isEdit])
return null;
}, [location, isEdit]);
const closeModal = useCallback(() => {
navigate(`/dashboard/shipments${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const { data: shipments, isPending } = useGetShipments(searchParams);
const { data: currentShipment } = useGetShipment(Number(editId), { enabled: !!editId });
const { data: currentShipment } = useGetShipment(Number(editId), {
enabled: !!editId,
});
const { data: allShipments } = useGetShipments();
const names = useMemo(() => {
return allShipments?.map((shipment: Shipment) => (shipment.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allShipments])
return allShipments
?.map((shipment: Shipment) => shipment.name)
.filter((season, index, array) => array.indexOf(season) === index);
}, [allShipments]);
const forms = useMemo(() => {
return allShipments?.map((shipment: Shipment) => (shipment.form.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allShipments])
return allShipments
?.map((shipment: Shipment) => shipment.form.name)
.filter((season, index, array) => array.indexOf(season) === index);
}, [allShipments]);
const createShipmentMutation = useCreateShipment();
const editShipmentMutation = useEditShipment();
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => {
const handleCreateShipment = useCallback(
async (shipment: ShipmentInputs) => {
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
closeModal();
}, [createShipmentMutation, closeModal]);
},
[createShipmentMutation, closeModal],
);
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => {
if (!id)
return;
const handleEditShipment = useCallback(
async (shipment: ShipmentInputs, id?: number) => {
if (!id) return;
await editShipmentMutation.mutateAsync({
id: id,
shipment: shipmentCreateFromShipmentInputs(shipment)
shipment: shipmentCreateFromShipmentInputs(shipment),
});
closeModal();
}, [editShipmentMutation, closeModal]);
},
[editShipmentMutation, closeModal],
);
const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => {
const onFilterChange = useCallback(
(values: string[], filter: string) => {
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.delete(filter)
params.delete(filter);
values.forEach(value => {
values.forEach((value) => {
params.append(filter, value);
});
return params;
});
}, [setSearchParams])
},
[setSearchParams],
);
if (!shipments || isPending)
return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
@@ -92,7 +108,9 @@ export default function Shipments() {
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/shipments/create${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/shipments/create${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconPlus />
@@ -129,14 +147,9 @@ export default function Shipments() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
shipments.map((shipment) => (
<ShipmentRow
shipment={shipment}
key={shipment.id}
/>
))
}
{shipments.map((shipment) => (
<ShipmentRow shipment={shipment} key={shipment.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>

View File

@@ -1,5 +1,3 @@
export default function Templates() {
return (
<></>
);
return <></>;
}

View File

@@ -19,63 +19,69 @@ export default function Users() {
const editId = useMemo(() => {
if (isEdit) {
return location.pathname.split("/")[3]
return location.pathname.split("/")[3];
}
return null
}, [location, isEdit])
return null;
}, [location, isEdit]);
const closeModal = useCallback(() => {
navigate(`/dashboard/users${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const { data: users, isPending } = useGetUsers(searchParams);
const { data: currentUser } = useGetUser(Number(editId), { enabled: !!editId });
const { data: currentUser } = useGetUser(Number(editId), {
enabled: !!editId,
});
const { data: allUsers } = useGetUsers();
const names = useMemo(() => {
return allUsers?.map((user: User) => (user.name))
.filter((season, index, array) => array.indexOf(season) === index)
}, [allUsers])
return allUsers
?.map((user: User) => user.name)
.filter((season, index, array) => array.indexOf(season) === index);
}, [allUsers]);
const createUserMutation = useCreateUser();
const editUserMutation = useEditUser();
const handleCreateUser = useCallback(async (user: UserInputs) => {
const handleCreateUser = useCallback(
async (user: UserInputs) => {
await createUserMutation.mutateAsync(user);
closeModal();
}, [createUserMutation, closeModal]);
},
[createUserMutation, closeModal],
);
const handleEditUser = useCallback(async (user: UserInputs, id?: number) => {
if (!id)
return;
const handleEditUser = useCallback(
async (user: UserInputs, id?: number) => {
if (!id) return;
await editUserMutation.mutateAsync({
id: id,
user: user
user: user,
});
closeModal();
}, [editUserMutation, closeModal]);
},
[editUserMutation, closeModal],
);
const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => {
const onFilterChange = useCallback(
(values: string[], filter: string) => {
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.delete(filter);
values.forEach(value => {
values.forEach((value) => {
params.append(filter, value);
});
return params;
});
}, [setSearchParams])
},
[setSearchParams],
);
if (!users || isPending)
return (
<Group
align="center"
justify="center"
h="80vh"
w="100%"
>
<Group align="center" justify="center" h="80vh" w="100%">
<Loader color="pink" />
</Group>
);
@@ -88,7 +94,9 @@ export default function Users() {
<ActionIcon
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/users/create${searchParams ? `?${searchParams.toString()}` : ""}`);
navigate(
`/dashboard/users/create${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}}
>
<IconPlus />
@@ -123,14 +131,9 @@ export default function Users() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
users.map((user) => (
<UserRow
user={user}
key={user.id}
/>
))
}
{users.map((user) => (
<UserRow user={user} key={user.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>

View File

@@ -1,6 +1,4 @@
import {
createBrowserRouter,
} from "react-router";
import { createBrowserRouter } from "react-router";
import Root from "@/root";
import { Home } from "@/pages/Home";
@@ -22,7 +20,8 @@ export const router = createBrowserRouter([
{ index: true, Component: Home },
{ path: "/forms", Component: Forms },
{
path: "/dashboard", Component: Dashboard,
path: "/dashboard",
Component: Dashboard,
children: [
{ path: "productors", Component: Productors },
{ path: "productors/create", Component: Productors },
@@ -40,7 +39,7 @@ export const router = createBrowserRouter([
{ path: "shipments", Component: Shipments },
{ path: "shipments/:id/edit", Component: Shipments },
{ path: "shipments/create", Component: Shipments },
]
],
},
{ path: "/form/:id", Component: Contract },
],

View File

@@ -1,8 +1,18 @@
import { useMutation, useQuery, useQueryClient,type DefinedInitialDataOptions,type UseQueryResult } from "@tanstack/react-query";
import {
useMutation,
useQuery,
useQueryClient,
type DefinedInitialDataOptions,
type UseQueryResult,
} from "@tanstack/react-query";
import { Config } from "@/config/config";
import type { Form, FormCreate, FormEditPayload } from "@/services/resources/forms";
import type { Shipment, ShipmentCreate, ShipmentEditPayload } from "@/services/resources/shipments";
import type { Productor, ProductorCreate, ProductorEditPayload } from "@/services/resources/productors";
import type {
Productor,
ProductorCreate,
ProductorEditPayload,
} from "@/services/resources/productors";
import type { User, UserCreate, UserEditPayload } from "@/services/resources/users";
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
import type { ContractCreate } from "./resources/contracts";
@@ -10,37 +20,37 @@ import { notifications } from "@mantine/notifications";
import { t } from "@/config/i18n";
export function useGetShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> {
const queryString = filters?.toString()
const queryString = filters?.toString();
return useQuery<Shipment[]>({
queryKey: ['shipments', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
queryKey: ["shipments", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`).then(
(res) => res.json(),
),
});
}
export function useGetShipment(id?: number, options?: Partial<DefinedInitialDataOptions<Shipment, Error, Shipment, readonly unknown[]>>): UseQueryResult<Shipment, Error> {
export function useGetShipment(
id?: number,
options?: Partial<DefinedInitialDataOptions<Shipment, Error, Shipment, readonly unknown[]>>,
): UseQueryResult<Shipment, Error> {
return useQuery<Shipment>({
queryKey: ['shipment'],
queryFn: () => (
fetch(`${Config.backend_uri}/shipments/${id}`)
.then((res) => res.json())
),
queryKey: ["shipment"],
queryFn: () => fetch(`${Config.backend_uri}/shipments/${id}`).then((res) => res.json()),
enabled: !!id,
...options,
});
}
export function useCreateShipment() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newShipment: ShipmentCreate) => {
return fetch(`${Config.backend_uri}/shipments`, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(newShipment),
}).then((res) => res.json());
@@ -50,27 +60,27 @@ export function useCreateShipment() {
title: t("success", { capfirst: true }),
message: t("successfully created shipment", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
await queryClient.invalidateQueries({ queryKey: ["shipments"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing shipment`, { capfirst: true }),
color: "red"
color: "red",
});
},
});
}
})
}
export function useEditShipment() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ shipment, id }: ShipmentEditPayload) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: 'PUT',
method: "PUT",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(shipment),
}).then((res) => res.json());
@@ -80,26 +90,26 @@ export function useEditShipment() {
title: t("success", { capfirst: true }),
message: t("successfully edited shipment", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
await queryClient.invalidateQueries({ queryKey: ["shipments"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing shipment`, { capfirst: true }),
color: "red"
color: "red",
});
},
});
}
})
}
export function useDeleteShipment() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: 'DELETE',
method: "DELETE",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
@@ -108,50 +118,50 @@ export function useDeleteShipment() {
title: t("success", { capfirst: true }),
message: t("successfully deleted shipment", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['shipments'] })
await queryClient.invalidateQueries({ queryKey: ["shipments"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting shipment`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Productor[], Error> {
const queryString = filters?.toString()
const queryString = filters?.toString();
return useQuery<Productor[]>({
queryKey: ['productors', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
queryKey: ["productors", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`).then(
(res) => res.json(),
),
});
}
export function useGetProductor(id?: number, options?: Partial<DefinedInitialDataOptions<Productor, Error, Productor, readonly unknown[]>>) {
export function useGetProductor(
id?: number,
options?: Partial<DefinedInitialDataOptions<Productor, Error, Productor, readonly unknown[]>>,
) {
return useQuery<Productor>({
queryKey: ['productor'],
queryFn: () => (
fetch(`${Config.backend_uri}/productors/${id}`)
.then((res) => res.json())
),
queryKey: ["productor"],
queryFn: () => fetch(`${Config.backend_uri}/productors/${id}`).then((res) => res.json()),
enabled: !!id,
...options,
});
}
export function useCreateProductor() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newProductor: ProductorCreate) => {
return fetch(`${Config.backend_uri}/productors`, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(newProductor),
}).then((res) => res.json());
@@ -161,27 +171,27 @@ export function useCreateProductor() {
title: t("success", { capfirst: true }),
message: t("successfully created productor", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['productors'] })
await queryClient.invalidateQueries({ queryKey: ["productors"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing productor`, { capfirst: true }),
color: "red"
color: "red",
});
},
});
}
})
}
export function useEditProductor() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ productor, id }: ProductorEditPayload) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
method: 'PUT',
method: "PUT",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(productor),
}).then((res) => res.json());
@@ -191,26 +201,26 @@ export function useEditProductor() {
title: t("success", { capfirst: true }),
message: t("successfully edited productor", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['productors'] })
await queryClient.invalidateQueries({ queryKey: ["productors"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing productor`, { capfirst: true }),
color: "red"
color: "red",
});
},
});
}
})
}
export function useDeleteProductor() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/productors/${id}`, {
method: 'DELETE',
method: "DELETE",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
@@ -219,68 +229,68 @@ export function useDeleteProductor() {
title: t("success", { capfirst: true }),
message: t("successfully deleted productor", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['productors'] })
await queryClient.invalidateQueries({ queryKey: ["productors"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting productor`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useGetForm(id?: number, options?: Partial<DefinedInitialDataOptions<Form, Error, Form, readonly unknown[]>>) {
export function useGetForm(
id?: number,
options?: Partial<DefinedInitialDataOptions<Form, Error, Form, readonly unknown[]>>,
) {
return useQuery<Form>({
queryKey: ['form'],
queryFn: () => (
fetch(`${Config.backend_uri}/forms/${id}`)
.then((res) => res.json())
),
queryKey: ["form"],
queryFn: () => fetch(`${Config.backend_uri}/forms/${id}`).then((res) => res.json()),
enabled: !!id,
...options,
});
}
export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> {
const queryString = filters?.toString()
const queryString = filters?.toString();
return useQuery<Form[]>({
queryKey: ['forms', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
queryKey: ["forms", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`).then((res) =>
res.json(),
),
});
}
export function useCreateForm() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newForm: FormCreate) => {
return fetch(`${Config.backend_uri}/forms`, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(newForm),
}).then((res) => res.json());
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['forms'] })
}
await queryClient.invalidateQueries({ queryKey: ["forms"] });
},
});
}
export function useDeleteForm() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/forms/${id}`, {
method: 'DELETE',
method: "DELETE",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
@@ -289,27 +299,27 @@ export function useDeleteForm() {
title: t("success", { capfirst: true }),
message: t("successfully deleted form", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['forms'] })
await queryClient.invalidateQueries({ queryKey: ["forms"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting form`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useEditForm() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, form }: FormEditPayload) => {
return fetch(`${Config.backend_uri}/forms/${id}`, {
method: 'PUT',
method: "PUT",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(form),
}).then((res) => res.json());
@@ -319,50 +329,50 @@ export function useEditForm() {
title: t("success", { capfirst: true }),
message: t("successfully edited form", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['forms'] })
await queryClient.invalidateQueries({ queryKey: ["forms"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing form`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useGetProduct(id?: number, options?: Partial<DefinedInitialDataOptions<Product, Error, Product, readonly unknown[]>>) {
export function useGetProduct(
id?: number,
options?: Partial<DefinedInitialDataOptions<Product, Error, Product, readonly unknown[]>>,
) {
return useQuery<Product>({
queryKey: ['product'],
queryFn: () => (
fetch(`${Config.backend_uri}/products/${id}`)
.then((res) => res.json())
),
queryKey: ["product"],
queryFn: () => fetch(`${Config.backend_uri}/products/${id}`).then((res) => res.json()),
enabled: !!id,
...options,
});
}
export function useGetProducts(filters?: URLSearchParams): UseQueryResult<Product[], Error> {
const queryString = filters?.toString()
const queryString = filters?.toString();
return useQuery<Product[]>({
queryKey: ['products', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
queryKey: ["products", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`).then((res) =>
res.json(),
),
});
}
export function useCreateProduct() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newProduct: ProductCreate) => {
return fetch(`${Config.backend_uri}/products`, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(newProduct),
}).then((res) => res.json());
@@ -372,26 +382,26 @@ export function useCreateProduct() {
title: t("success", { capfirst: true }),
message: t("successfully created product", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['products'] })
await queryClient.invalidateQueries({ queryKey: ["products"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing product`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useDeleteProduct() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
method: 'DELETE',
method: "DELETE",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
@@ -400,27 +410,27 @@ export function useDeleteProduct() {
title: t("success", { capfirst: true }),
message: t("successfully deleted product", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['products'] })
await queryClient.invalidateQueries({ queryKey: ["products"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting product`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useEditProduct() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, product }: ProductEditPayload) => {
return fetch(`${Config.backend_uri}/products/${id}`, {
method: 'PUT',
method: "PUT",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(product),
}).then((res) => res.json());
@@ -430,50 +440,50 @@ export function useEditProduct() {
title: t("success", { capfirst: true }),
message: t("successfully edited product", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['products'] })
await queryClient.invalidateQueries({ queryKey: ["products"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing product`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useGetUser(id?: number, options?: Partial<DefinedInitialDataOptions<User, Error, User, readonly unknown[]>>) {
export function useGetUser(
id?: number,
options?: Partial<DefinedInitialDataOptions<User, Error, User, readonly unknown[]>>,
) {
return useQuery<User>({
queryKey: ['user'],
queryFn: () => (
fetch(`${Config.backend_uri}/users/${id}`)
.then((res) => res.json())
),
queryKey: ["user"],
queryFn: () => fetch(`${Config.backend_uri}/users/${id}`).then((res) => res.json()),
enabled: !!id,
...options,
});
}
export function useGetUsers(filters?: URLSearchParams): UseQueryResult<User[], Error> {
const queryString = filters?.toString()
const queryString = filters?.toString();
return useQuery<User[]>({
queryKey: ['users', queryString],
queryFn: () => (
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`)
.then((res) => res.json())
queryKey: ["users", queryString],
queryFn: () =>
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`).then((res) =>
res.json(),
),
});
}
export function useCreateUser() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newUser: UserCreate) => {
return fetch(`${Config.backend_uri}/users`, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(newUser),
}).then((res) => res.json());
@@ -483,26 +493,26 @@ export function useCreateUser() {
title: t("success", { capfirst: true }),
message: t("successfully created user", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['users'] })
await queryClient.invalidateQueries({ queryKey: ["users"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing user`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useDeleteUser() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/users/${id}`, {
method: 'DELETE',
method: "DELETE",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
}).then((res) => res.json());
},
@@ -511,27 +521,27 @@ export function useDeleteUser() {
title: t("success", { capfirst: true }),
message: t("successfully deleted user", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['users'] })
await queryClient.invalidateQueries({ queryKey: ["users"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting user`, { capfirst: true }),
color: "red"
color: "red",
});
}
},
});
}
export function useEditUser() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, user }: UserEditPayload) => {
return fetch(`${Config.backend_uri}/users/${id}`, {
method: 'PUT',
method: "PUT",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(user),
}).then((res) => res.json());
@@ -541,28 +551,27 @@ export function useEditUser() {
title: t("success", { capfirst: true }),
message: t("successfully edited user", { capfirst: true }),
});
await queryClient.invalidateQueries({ queryKey: ['users'] })
await queryClient.invalidateQueries({ queryKey: ["users"] });
},
onError: (error) => {
notifications.show({
title: t("error", { capfirst: true }),
message: error?.message || t(`error editing user`, { capfirst: true }),
color: "red"
color: "red",
});
},
});
}
});
}
export function useCreateContract() {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newContract: ContractCreate) => {
return fetch(`${Config.backend_uri}/contracts`, {
method: 'POST',
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify(newContract),
}).then(async (res) => await res.blob());
@@ -575,6 +584,6 @@ export function useCreateContract() {
link.click();
URL.revokeObjectURL(url);
await queryClient.invalidateQueries({ queryKey: ["contracts"] });
}
},
});
}

View File

@@ -1,4 +1,4 @@
export type ContractCreate = {
form_id: number;
contract: Record<string, string | number | null>;
}
};

View File

@@ -12,7 +12,7 @@ export type Form = {
referer: User;
shipments: Shipment[];
minimum_shipment_value: number | null;
}
};
export type FormCreate = {
name: string;
@@ -22,7 +22,7 @@ export type FormCreate = {
productor_id: number;
referer_id: number;
minimum_shipment_value: number | null;
}
};
export type FormEdit = {
name?: string | null;
@@ -32,12 +32,12 @@ export type FormEdit = {
productor_id?: number | null;
referer_id?: number | null;
minimum_shipment_value: number | null;
}
};
export type FormEditPayload = {
id: number;
form: FormEdit;
}
};
export type FormInputs = {
name: string;
@@ -47,4 +47,4 @@ export type FormInputs = {
productor_id: string;
referer_id: string;
minimum_shipment_value: number | string | null;
}
};

View File

@@ -4,12 +4,12 @@ import type { Product } from "./products";
export const PaymentMethods = [
{ value: "cheque", label: t("cheque", { capfirst: true }) },
{ value: "transfer", label: t("transfer", { capfirst: true }) },
]
];
export type PaymentMethod = {
name: string;
details: string;
}
};
export type Productor = {
id: number;
@@ -17,31 +17,31 @@ export type Productor = {
address: string;
payment_methods: PaymentMethod[];
type: string;
products: Product[]
}
products: Product[];
};
export type ProductorCreate = {
name: string;
address: string;
payment_methods: PaymentMethod[];
type: string;
}
};
export type ProductorEdit = {
name: string | null;
address: string | null;
payment_methods: PaymentMethod[];
type: string | null;
}
};
export type ProductorInputs = {
name: string;
address: string;
type: string;
payment_methods: PaymentMethod[];
}
};
export type ProductorEditPayload = {
productor: ProductorEdit;
id: number;
}
};

View File

@@ -16,11 +16,11 @@ export const ProductUnit = {
};
export const ProductQuantityUnit = {
"ml": "mililiter",
"L": "liter",
"g": "grams",
"kg": "kilo"
}
ml: "mililiter",
L: "liter",
g: "grams",
kg: "kilo",
};
export type Product = {
id: number;
@@ -33,7 +33,7 @@ export type Product = {
quantity_unit: string | null;
type: ProductTypeKey;
shipments: Shipment[];
}
};
export type ProductCreate = {
productor_id: number;
@@ -44,7 +44,7 @@ export type ProductCreate = {
quantity: number | null;
quantity_unit: string | null;
type: string;
}
};
export type ProductEdit = {
productor_id: number | null;
@@ -55,7 +55,7 @@ export type ProductEdit = {
quantity: number | null;
quantity_unit: string | null;
type: string | null;
}
};
export type ProductInputs = {
productor_id: string | null;
@@ -66,12 +66,12 @@ export type ProductInputs = {
quantity: number | string | null;
quantity_unit: string | null;
type: string | null;
}
};
export type ProductEditPayload = {
product: ProductEdit;
id: number;
}
};
export function productToProductInputs(product: Product): ProductInputs {
return {
@@ -92,9 +92,15 @@ export function productCreateFromProductInputs(productInput: ProductInputs): Pro
name: productInput.name,
unit: productInput.unit!,
price: productInput.price === "" || !productInput.price ? null : Number(productInput.price),
price_kg: productInput.price_kg === "" || !productInput.price_kg ? null : Number(productInput.price_kg),
quantity: productInput.quantity === "" || !productInput.quantity ? null : Number(productInput.quantity),
price_kg:
productInput.price_kg === "" || !productInput.price_kg
? null
: Number(productInput.price_kg),
quantity:
productInput.quantity === "" || !productInput.quantity
? null
: Number(productInput.quantity),
quantity_unit: productInput.quantity_unit,
type: productInput.type!,
}
};
}

View File

@@ -8,39 +8,39 @@ export type Shipment = {
form: Form;
form_id: number;
products: Product[];
}
};
export type ShipmentCreate = {
name: string;
date: string;
form_id: number;
product_ids: number[];
}
};
export type ShipmentEdit = {
name: string | null;
date: string | null;
form_id: number | null;
product_ids: number[];
}
};
export type ShipmentEditPayload = {
id: number;
shipment: ShipmentEdit;
}
};
export type ShipmentInputs = {
name: string | null;
date: string | null;
form_id: string | null;
product_ids: string[];
}
};
export function shipmentToShipmentInputs(shipment: Shipment): ShipmentInputs {
return {
...shipment,
form_id: String(shipment.form_id),
product_ids: shipment.products.map((el) => (String(el.id)))
product_ids: shipment.products.map((el) => String(el.id)),
};
}
@@ -49,6 +49,6 @@ export function shipmentCreateFromShipmentInputs(shipmentInput: ShipmentInputs):
name: shipmentInput.name!,
date: shipmentInput.date!,
form_id: Number(shipmentInput.form_id),
product_ids: shipmentInput.product_ids.map(el => (Number(el))),
}
product_ids: shipmentInput.product_ids.map((el) => Number(el)),
};
}

View File

@@ -5,24 +5,24 @@ export type User = {
name: string;
email: string;
products: Product[];
}
};
export type UserInputs = {
email: string;
name: string;
}
};
export type UserCreate = {
email: string | null;
name: string | null;
}
};
export type UserEdit = {
email: string | null;
name: string | null;
}
};
export type UserEditPayload = {
user: UserEdit;
id: number;
}
};

View File

@@ -1,4 +1,4 @@
import { createTheme } from '@mantine/core';
import { createTheme } from "@mantine/core";
export const theme = createTheme({
/** Put your mantine theme override here */

View File

@@ -1,7 +1,4 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

View File

@@ -1,18 +1,18 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path';
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
"@": path.resolve(__dirname, "src"),
},
},
server: {
watch: {
usePolling: true,
},
}
})
},
});