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,104 +1,103 @@
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: "latest",
sourceType: 'module', sourceType: "module",
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
}, },
project: './tsconfig.json', // Required for type-aware rules project: "./tsconfig.json", // Required for type-aware rules
}, },
env: { env: {
browser: true, browser: true,
es2021: true, es2021: true,
node: true, node: true,
}, },
plugins: [ plugins: ["react", "react-hooks", "@typescript-eslint", "jsx-a11y", "import", "unused-imports"],
'react', extends: [
'react-hooks', "eslint:recommended",
'@typescript-eslint', "plugin:react/recommended",
'jsx-a11y', "plugin:react-hooks/recommended",
'import', "plugin:@typescript-eslint/recommended",
'unused-imports', "plugin:@typescript-eslint/recommended-requiring-type-checking",
], "plugin:jsx-a11y/recommended",
extends: [ "plugin:import/errors",
'eslint:recommended', "plugin:import/warnings",
'plugin:react/recommended', "plugin:import/typescript",
'plugin:react-hooks/recommended', "prettier",
'plugin:@typescript-eslint/recommended', ],
'plugin:@typescript-eslint/recommended-requiring-type-checking', rules: {
'plugin:jsx-a11y/recommended', "@typescript-eslint/no-unused-vars": [
'plugin:import/errors', "error",
'plugin:import/warnings', { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
'plugin:import/typescript', ],
'prettier', "@typescript-eslint/no-explicit-any": "error",
], "@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: false }],
rules: { "@typescript-eslint/strict-boolean-expressions": "error",
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], "@typescript-eslint/no-floating-promises": "error",
'@typescript-eslint/no-explicit-any': 'error', "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
'@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: false }], "@typescript-eslint/no-misused-promises": "error",
'@typescript-eslint/strict-boolean-expressions': 'error', "@typescript-eslint/prefer-readonly": "error",
'@typescript-eslint/no-floating-promises': 'error', "@typescript-eslint/explicit-module-boundary-types": "error",
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], "@typescript-eslint/typedef": [
'@typescript-eslint/no-misused-promises': 'error', "error",
'@typescript-eslint/prefer-readonly': 'error', {
'@typescript-eslint/explicit-module-boundary-types': 'error', arrayDestructuring: true,
'@typescript-eslint/typedef': [ arrowParameter: true,
'error', memberVariableDeclaration: true,
{ objectDestructuring: true,
arrayDestructuring: true, parameter: true,
arrowParameter: true, propertyDeclaration: true,
memberVariableDeclaration: true, variableDeclaration: true,
objectDestructuring: true, variableDeclarationIgnoreFunction: false,
parameter: true, },
propertyDeclaration: true, ],
variableDeclaration: true,
variableDeclarationIgnoreFunction: false, "react/react-in-jsx-scope": "off",
}, "react/prop-types": "off",
], "react/jsx-uses-react": "off",
"react/jsx-uses-vars": "error",
'react/react-in-jsx-scope': 'off', "react/jsx-no-useless-fragment": "error",
'react/prop-types': 'off', "react/self-closing-comp": "error",
'react/jsx-uses-react': 'off',
'react/jsx-uses-vars': 'error', "react-hooks/rules-of-hooks": "error",
'react/jsx-no-useless-fragment': 'error', "react-hooks/exhaustive-deps": "error",
'react/self-closing-comp': 'error',
"jsx-a11y/no-noninteractive-element-interactions": "error",
'react-hooks/rules-of-hooks': 'error', "jsx-a11y/anchor-is-valid": "error",
'react-hooks/exhaustive-deps': 'error', "jsx-a11y/click-events-have-key-events": "error",
'jsx-a11y/no-noninteractive-element-interactions': 'error', "import/order": [
'jsx-a11y/anchor-is-valid': 'error', "error",
'jsx-a11y/click-events-have-key-events': 'error', {
groups: [
'import/order': [ ["builtin", "external"],
'error', ["internal", "parent", "sibling", "index"],
{ ],
groups: [['builtin', 'external'], ['internal', 'parent', 'sibling', 'index']], "newlines-between": "always",
'newlines-between': 'always', alphabetize: { order: "asc", caseInsensitive: true },
alphabetize: { order: 'asc', caseInsensitive: true }, },
}, ],
], "import/no-unresolved": "error",
'import/no-unresolved': 'error', "import/no-duplicates": "error",
'import/no-duplicates': 'error',
"unused-imports/no-unused-imports-ts": "error",
'unused-imports/no-unused-imports-ts': 'error', "no-console": ["warn", { allow: ["warn", "error"] }],
'no-console': ['warn', { allow: ['warn', 'error'] }], "no-debugger": "error",
'no-debugger': 'error', eqeqeq: ["error", "always"],
'eqeqeq': ['error', 'always'], curly: "error",
'curly': 'error', semi: ["error", "always"],
'semi': ['error', 'always'], quotes: ["error", "single", { avoidEscape: true }],
'quotes': ['error', 'single', { avoidEscape: true }], "prefer-const": "error",
'prefer-const': 'error', "no-var": "error",
'no-var': 'error', },
}, settings: {
settings: { react: {
react: { version: "detect",
version: 'detect', },
}, "import/resolver": {
'import/resolver': { typescript: {},
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,57 +17,57 @@ If you are developing a production application, we recommend updating the config
```js ```js
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(["dist"]),
{ {
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
// Other configs... // Other configs...
// Remove tseslint.configs.recommended and replace with this // Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked, tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules // Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked, tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules // Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked, tseslint.configs.stylisticTypeChecked,
// Other configs... // Other configs...
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'], project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
// other options... // 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: 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 ```js
// eslint.config.js // eslint.config.js
import reactX from 'eslint-plugin-react-x' import reactX from "eslint-plugin-react-x";
import reactDom from 'eslint-plugin-react-dom' import reactDom from "eslint-plugin-react-dom";
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(["dist"]),
{ {
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
// Other configs... // Other configs...
// Enable lint rules for React // Enable lint rules for React
reactX.configs['recommended-typescript'], reactX.configs["recommended-typescript"],
// Enable lint rules for React DOM // Enable lint rules for React DOM
reactDom.configs.recommended, reactDom.configs.recommended,
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'], project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
// other options... // other options...
},
}, },
}, ]);
])
``` ```

View File

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

View File

@@ -1,13 +1,13 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Amap Croix-luizet</title> <title>Amap Croix-luizet</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -124,4 +124,4 @@
"there is no contract for now": "there is no contract for now.", "there is no contract for now": "there is no contract for now.",
"the product unit will be assigned to the quantity requested in the form": "the product unit will be assigned to the quantity requested in the form", "the product unit will be assigned to the quantity requested in the form": "the product unit will be assigned to the quantity requested in the form",
"all theses informations are for contract generation": "all theses informations are for contract generation." "all theses informations are for contract generation": "all theses informations are for contract generation."
} }

View File

@@ -136,4 +136,4 @@
"there is no contract for now": "Il n'y a pas de contrats pour le moment.", "there is no contract for now": "Il n'y a pas de contrats pour le moment.",
"the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée a la quantité demandée dans le formulaire des amapiens.", "the product unit will be assigned to the quantity requested in the form": "L'unité de vente du produit définit l'unité associée a la quantité demandée dans le formulaire des amapiens.",
"all theses informations are for contract generation": "ces informations sont nécéssaires pour la génération de contrat." "all theses informations are for contract generation": "ces informations sont nécéssaires pour la génération de contrat."
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,52 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "format": "prettier . --write",
}, "preview": "vite preview"
"dependencies": { },
"@mantine/core": "^8.3.14", "dependencies": {
"@mantine/dates": "^8.3.14", "@mantine/core": "^8.3.14",
"@mantine/form": "^8.3.14", "@mantine/dates": "^8.3.14",
"@mantine/hooks": "^8.3.14", "@mantine/form": "^8.3.14",
"@mantine/notifications": "^8.3.14", "@mantine/hooks": "^8.3.14",
"@tabler/icons": "^3.36.1", "@mantine/notifications": "^8.3.14",
"@tabler/icons-react": "^3.36.1", "@tabler/icons": "^3.36.1",
"@tanstack/react-query": "^5.90.20", "@tabler/icons-react": "^3.36.1",
"capitalize": "^2.0.4", "@tanstack/react-query": "^5.90.20",
"dayjs": "^1.11.19", "capitalize": "^2.0.4",
"i18next": "^25.8.4", "dayjs": "^1.11.19",
"i18next-browser-languagedetector": "^8.2.0", "i18next": "^25.8.4",
"luxon": "^3.7.2", "i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.0", "luxon": "^3.7.2",
"react-dom": "^19.2.0", "react": "^19.2.0",
"react-i18next": "^16.5.4", "react-dom": "^19.2.0",
"react-router": "^7.13.0" "react-i18next": "^16.5.4",
}, "react-router": "^7.13.0"
"devDependencies": { },
"@eslint/js": "^9.39.1", "devDependencies": {
"@types/capitalize": "^2.0.2", "@eslint/js": "^9.39.1",
"@types/luxon": "^3.7.1", "@types/capitalize": "^2.0.2",
"@types/node": "^24.10.1", "@types/luxon": "^3.7.1",
"@types/react": "^19.2.7", "@types/node": "^24.10.1",
"@types/react-dom": "^19.2.3", "@types/react": "^19.2.7",
"@vitejs/plugin-react": "^5.1.1", "@types/react-dom": "^19.2.3",
"eslint": "^9.39.1", "@vitejs/plugin-react": "^5.1.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint": "^9.39.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0", "eslint-plugin-react-refresh": "^0.4.24",
"postcss": "^8.5.6", "globals": "^16.5.0",
"postcss-preset-mantine": "^1.18.0", "postcss": "^8.5.6",
"postcss-simple-vars": "^7.0.1", "postcss-preset-mantine": "^1.18.0",
"typescript": "~5.9.3", "postcss-simple-vars": "^7.0.1",
"typescript-eslint": "^8.48.0", "prettier": "3.8.1",
"vite": "^7.3.1" "typescript": "~5.9.3",
} "typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
}
} }

View File

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

View File

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

View File

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

View File

@@ -1,49 +1,49 @@
import { Group, MultiSelect } from "@mantine/core"; import { Group, MultiSelect } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useMemo } from "react"; import { useMemo } from "react";
export type FilterFormsProps = { export type FilterFormsProps = {
seasons: string[]; seasons: string[];
productors: string[]; productors: string[];
filters: URLSearchParams; filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} };
export default function FilterForms({ export default function FilterForms({
seasons, seasons,
productors, productors,
filters, filters,
onFilterChange onFilterChange,
}: FilterFormsProps) { }: FilterFormsProps) {
const defaultProductors = useMemo(() => { const defaultProductors = useMemo(() => {
return filters.getAll("productors") return filters.getAll("productors");
}, [filters]); }, [filters]);
const defaultSeasons = useMemo(() => { const defaultSeasons = useMemo(() => {
return filters.getAll("seasons") return filters.getAll("seasons");
}, [filters]); }, [filters]);
return ( return (
<Group> <Group>
<MultiSelect <MultiSelect
aria-label={t("filter by season", {capfirst: true})} aria-label={t("filter by season", { capfirst: true })}
placeholder={t("filter by season", {capfirst: true})} placeholder={t("filter by season", { capfirst: true })}
data={seasons} data={seasons}
defaultValue={defaultSeasons} defaultValue={defaultSeasons}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'seasons') onFilterChange(values, "seasons");
}} }}
clearable clearable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by productor", {capfirst: true})} aria-label={t("filter by productor", { capfirst: true })}
placeholder={t("filter by productor", {capfirst: true})} placeholder={t("filter by productor", { capfirst: true })}
data={productors} data={productors}
defaultValue={defaultProductors} defaultValue={defaultProductors}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'productors') onFilterChange(values, "productors");
}} }}
clearable clearable
/> />
</Group> </Group>
); );
} }

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 { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react"; import { IconCancel, IconEdit, IconPlus } from "@tabler/icons-react";
@@ -10,16 +18,11 @@ import type { Form, FormInputs } from "@/services/resources/forms";
export type FormModalProps = ModalBaseProps & { export type FormModalProps = ModalBaseProps & {
currentForm?: Form; currentForm?: Form;
handleSubmit: (form: FormInputs, id?: number) => void; handleSubmit: (form: FormInputs, id?: number) => void;
} };
export default function FormModal({ export default function FormModal({ opened, onClose, currentForm, handleSubmit }: FormModalProps) {
opened, const { data: productors } = useGetProductors();
onClose, const { data: users } = useGetUsers();
currentForm,
handleSubmit
}: FormModalProps) {
const {data: productors} = useGetProductors();
const {data: users} = useGetUsers();
const form = useForm<FormInputs>({ const form = useForm<FormInputs>({
initialValues: { initialValues: {
@@ -33,115 +36,138 @@ export default function FormModal({
}, },
validate: { validate: {
name: (value) => name: (value) =>
!value ? `${t("a name", {capfirst: true})} ${t('is required')}` : null, !value ? `${t("a name", { capfirst: true })} ${t("is required")}` : null,
season: (value) => season: (value) =>
!value ? `${t("a season", {capfirst: true})} ${t('is required')}` : null, !value ? `${t("a season", { capfirst: true })} ${t("is required")}` : null,
start: (value) => 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) => 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) => 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) => 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(() => { 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]); }, [users]);
const productorsSelect = useMemo(() => { 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]); }, [productors]);
return ( return (
<Modal <Modal
opened={opened} opened={opened}
onClose={onClose} 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 <TextInput
label={t("form name", {capfirst: true})} label={t("form name", { capfirst: true })}
placeholder={t("form name", {capfirst: true})} placeholder={t("form name", { capfirst: true })}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('name')} {...form.getInputProps("name")}
/> />
<TextInput <TextInput
label={t("contract season", {capfirst: true})} label={t("contract season", { capfirst: true })}
placeholder={t("contract season", {capfirst: true})} placeholder={t("contract season", { capfirst: true })}
description={t("contract season recommandation", {capfirst: true})} description={t("contract season recommandation", { capfirst: true })}
radius="sm" radius="sm"
withAsterisk withAsterisk
{...form.getInputProps('season')} {...form.getInputProps("season")}
/> />
<Group grow> <Group grow>
<DatePickerInput <DatePickerInput
label={t("start date", {capfirst: true})} label={t("start date", { capfirst: true })}
placeholder={t("start date", {capfirst: true})} placeholder={t("start date", { capfirst: true })}
withAsterisk withAsterisk
{...form.getInputProps('start')} {...form.getInputProps("start")}
/> />
<DatePickerInput <DatePickerInput
label={t("end date", {capfirst: true})} label={t("end date", { capfirst: true })}
placeholder={t("end date", {capfirst: true})} placeholder={t("end date", { capfirst: true })}
withAsterisk withAsterisk
{...form.getInputProps('end')} {...form.getInputProps("end")}
/> />
</Group> </Group>
<Select <Select
label={t("referer", {capfirst: true})} label={t("referer", { capfirst: true })}
placeholder={t("referer", {capfirst: true})} placeholder={t("referer", { capfirst: true })}
nothingFoundMessage={t("nothing found", {capfirst: true})} nothingFoundMessage={t("nothing found", { capfirst: true })}
withAsterisk withAsterisk
clearable clearable
allowDeselect allowDeselect
searchable searchable
data={usersSelect || []} data={usersSelect || []}
{...form.getInputProps('referer_id')} {...form.getInputProps("referer_id")}
/> />
<Select <Select
label={t("productor", {capfirst: true})} label={t("productor", { capfirst: true })}
placeholder={t("productor", {capfirst: true})} placeholder={t("productor", { capfirst: true })}
nothingFoundMessage={t("nothing found", {capfirst: true})} nothingFoundMessage={t("nothing found", { capfirst: true })}
withAsterisk withAsterisk
clearable clearable
allowDeselect allowDeselect
searchable searchable
data={productorsSelect || []} data={productorsSelect || []}
{...form.getInputProps('productor_id')} {...form.getInputProps("productor_id")}
/> />
<NumberInput <NumberInput
label={t("minimum shipment value", {capfirst: true})} label={t("minimum shipment value", { capfirst: true })}
placeholder={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" radius="sm"
{...form.getInputProps('minimum_shipment_value')} {...form.getInputProps("minimum_shipment_value")}
/> />
<Group mt="sm" justify="space-between"> <Group mt="sm" justify="space-between">
<Button <Button
variant="filled" variant="filled"
color="red" color="red"
aria-label={t("cancel", {capfirst: true})} aria-label={t("cancel", { capfirst: true })}
leftSection={<IconCancel/>} leftSection={<IconCancel />}
onClick={() => { onClick={() => {
form.clearErrors(); form.clearErrors();
onClose(); onClose();
}} }}
>{t("cancel", {capfirst: true})}</Button> >
{t("cancel", { capfirst: true })}
</Button>
<Button <Button
variant="filled" variant="filled"
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})} aria-label={
leftSection={currentForm ? <IconEdit/> : <IconPlus/>} currentForm
? t("edit form", { capfirst: true })
: t("create form", { capfirst: true })
}
leftSection={currentForm ? <IconEdit /> : <IconPlus />}
onClick={() => { onClick={() => {
form.validate(); form.validate();
if (form.isValid()) { 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> </Group>
</Modal> </Modal>
); );
} }

View File

@@ -1,17 +1,15 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { useDeleteForm} from "@/services/api"; import { useDeleteForm } from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import type { Form } from "@/services/resources/forms"; import type { Form } from "@/services/resources/forms";
export type FormRowProps = { export type FormRowProps = {
form: Form; form: Form;
} };
export default function FormRow({ export default function FormRow({ form }: FormRowProps) {
form,
}: FormRowProps) {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const deleteMutation = useDeleteForm(); const deleteMutation = useDeleteForm();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -25,31 +23,33 @@ export default function FormRow({
<Table.Td>{form.productor.name}</Table.Td> <Table.Td>{form.productor.name}</Table.Td>
<Table.Td>{form.referer.name}</Table.Td> <Table.Td>{form.referer.name}</Table.Td>
<Table.Td> <Table.Td>
<Tooltip label={t("edit productor", {capfirst: true})}> <Tooltip label={t("edit productor", { capfirst: true })}>
<ActionIcon <ActionIcon
size="sm" size="sm"
mr="5" mr="5"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/dashboard/forms/${form.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`); navigate(
`/dashboard/forms/${form.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
);
}} }}
> >
<IconEdit/> <IconEdit />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("remove productor", {capfirst: true})}> <Tooltip label={t("remove productor", { capfirst: true })}>
<ActionIcon <ActionIcon
color="red" color="red"
size="sm" size="sm"
mr="5" mr="5"
onClick={() => { onClick={() => {
deleteMutation.mutate(form.id); deleteMutation.mutate(form.id);
}} }}
> >
<IconX/> <IconX />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
); );
} }

View File

@@ -1,28 +1,24 @@
import { ActionIcon, Tooltip } from "@mantine/core"; import { ActionIcon, Tooltip } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
export type InputLabelProps = { export type InputLabelProps = {
label: string; label: string;
info: string; info: string;
isRequired?: boolean; isRequired?: boolean;
} };
export function InputLabel({label, info, isRequired}: InputLabelProps) { export function InputLabel({ label, info, isRequired }: InputLabelProps) {
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<Tooltip label={info}> <Tooltip label={info}>
<ActionIcon variant="transparent" size="xs" color="gray"> <ActionIcon variant="transparent" size="xs" color="gray">
<IconInfoCircle size={16}/> <IconInfoCircle size={16} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<span> <span>
{label} {label}
{ {isRequired ? <span style={{ color: "red" }}> *</span> : null}
isRequired ? </span>
<span style={{ color: 'red' }}> *</span> : null </div>
} );
</span> }
</div>
);
}

View File

@@ -6,8 +6,8 @@ nav {
justify-content: space-between; justify-content: space-between;
} }
.navLink { .navLink {
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
} }

View File

@@ -8,28 +8,24 @@ export function Navbar() {
return ( return (
<nav> <nav>
<Group> <Group>
<NavLink <NavLink className={"navLink"} aria-label={t("home")} to="/">
className={"navLink"} {t("home", { capfirst: true })}
aria-label={t('home')}
to="/"
>
{t("home", {capfirst: true})}
</NavLink> </NavLink>
<NavLink <NavLink
className={"navLink"} className={"navLink"}
aria-label={t('dashboard')} aria-label={t("dashboard")}
to="/dashboard/productors" to="/dashboard/productors"
> >
{t("dashboard", {capfirst: true})} {t("dashboard", { capfirst: true })}
</NavLink> </NavLink>
</Group> </Group>
<NavLink <NavLink
className={"navLink"} className={"navLink"}
aria-label={t("login with keycloak")} aria-label={t("login with keycloak")}
to={`${Config.backend_uri}/auth/login`} to={`${Config.backend_uri}/auth/login`}
> >
{t("login with keycloak", {capfirst: true})} {t("login with keycloak", { capfirst: true })}
</NavLink> </NavLink>
</nav> </nav>
); );
} }

View File

@@ -1,51 +1,51 @@
import { Group, MultiSelect } from "@mantine/core"; import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react"; import { useMemo } from "react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
export type ProductorsFiltersProps = { export type ProductorsFiltersProps = {
names: string[]; names: string[];
types: string[]; types: string[];
filters: URLSearchParams; filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} };
export default function ProductorsFilter({ export default function ProductorsFilter({
names, names,
types, types,
filters, filters,
onFilterChange onFilterChange,
}: ProductorsFiltersProps) { }: ProductorsFiltersProps) {
const defaultNames = useMemo(() => { const defaultNames = useMemo(() => {
return filters.getAll("names") return filters.getAll("names");
}, [filters]); }, [filters]);
const defaultTypes = useMemo(() => { const defaultTypes = useMemo(() => {
return filters.getAll("types") return filters.getAll("types");
}, [filters]); }, [filters]);
return ( return (
<Group> <Group>
<MultiSelect <MultiSelect
aria-label={t("filter by name", {capfirst: true})} aria-label={t("filter by name", { capfirst: true })}
placeholder={t("filter by name", {capfirst: true})} placeholder={t("filter by name", { capfirst: true })}
data={names} data={names}
defaultValue={defaultNames} defaultValue={defaultNames}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'names') onFilterChange(values, "names");
}} }}
clearable clearable
searchable searchable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by type", {capfirst: true})} aria-label={t("filter by type", { capfirst: true })}
placeholder={t("filter by type", {capfirst: true})} placeholder={t("filter by type", { capfirst: true })}
data={types} data={types}
defaultValue={defaultTypes} defaultValue={defaultTypes}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'types') onFilterChange(values, "types");
}} }}
clearable clearable
searchable searchable
/> />
</Group> </Group>
); );
} }

View File

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

View File

@@ -1,62 +1,59 @@
import { ActionIcon, Badge, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Badge, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import type { Productor } from "@/services/resources/productors"; import type { Productor } from "@/services/resources/productors";
import { useDeleteProductor } from "@/services/api"; import { useDeleteProductor } from "@/services/api";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
export type ProductorRowProps = { export type ProductorRowProps = {
productor: Productor; productor: Productor;
} };
export default function ProductorRow({ export default function ProductorRow({ productor }: ProductorRowProps) {
productor, const [searchParams] = useSearchParams();
}: ProductorRowProps) { const deleteMutation = useDeleteProductor();
const [searchParams] = useSearchParams(); const navigate = useNavigate();
const deleteMutation = useDeleteProductor();
const navigate = useNavigate(); return (
<Table.Tr key={productor.id}>
return ( <Table.Td>{productor.name}</Table.Td>
<Table.Tr key={productor.id}> <Table.Td>{productor.type}</Table.Td>
<Table.Td>{productor.name}</Table.Td> <Table.Td>{productor.address}</Table.Td>
<Table.Td>{productor.type}</Table.Td> <Table.Td>
<Table.Td>{productor.address}</Table.Td> {productor.payment_methods.map((value) => (
<Table.Td> <Badge key={value.name} ml="xs">
{ {t(value.name, { capfirst: true })}
productor.payment_methods.map((value) =>( </Badge>
<Badge key={value.name} ml="xs"> ))}
{t(value.name, {capfirst: true})} </Table.Td>
</Badge> <Table.Td>
)) <Tooltip label={t("edit productor", { capfirst: true })}>
} <ActionIcon
</Table.Td> size="sm"
<Table.Td> mr="5"
<Tooltip label={t("edit productor", {capfirst: true})}> onClick={(e) => {
<ActionIcon e.stopPropagation();
size="sm" navigate(
mr="5" `/dashboard/productors/${productor.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
onClick={(e) => { );
e.stopPropagation(); }}
navigate(`/dashboard/productors/${productor.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`); >
}} <IconEdit />
> </ActionIcon>
<IconEdit/> </Tooltip>
</ActionIcon> <Tooltip label={t("remove productor", { capfirst: true })}>
</Tooltip> <ActionIcon
<Tooltip label={t("remove productor", {capfirst: true})}> color="red"
<ActionIcon size="sm"
color="red" mr="5"
size="sm" onClick={() => {
mr="5" deleteMutation.mutate(productor.id);
onClick={() => { }}
deleteMutation.mutate(productor.id); >
}} <IconX />
> </ActionIcon>
<IconX/> </Tooltip>
</ActionIcon> </Table.Td>
</Tooltip> </Table.Tr>
</Table.Td> );
</Table.Tr> }
);
}

View File

@@ -1,51 +1,51 @@
import { Group, MultiSelect } from "@mantine/core"; import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react"; import { useMemo } from "react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
export type ProductsFiltersProps = { export type ProductsFiltersProps = {
names: string[]; names: string[];
productors: string[]; productors: string[];
filters: URLSearchParams; filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} };
export default function ProductsFilters({ export default function ProductsFilters({
names, names,
productors, productors,
filters, filters,
onFilterChange onFilterChange,
}: ProductsFiltersProps) { }: ProductsFiltersProps) {
const defaultNames = useMemo(() => { const defaultNames = useMemo(() => {
return filters.getAll("names") return filters.getAll("names");
}, [filters]); }, [filters]);
const defaultProductors = useMemo(() => { const defaultProductors = useMemo(() => {
return filters.getAll("productors") return filters.getAll("productors");
}, [filters]); }, [filters]);
return ( return (
<Group> <Group>
<MultiSelect <MultiSelect
aria-label={t("filter by name", {capfirst: true})} aria-label={t("filter by name", { capfirst: true })}
placeholder={t("filter by name", {capfirst: true})} placeholder={t("filter by name", { capfirst: true })}
data={names} data={names}
defaultValue={defaultNames} defaultValue={defaultNames}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'names') onFilterChange(values, "names");
}} }}
clearable clearable
searchable searchable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by productor", {capfirst: true})} aria-label={t("filter by productor", { capfirst: true })}
placeholder={t("filter by productor", {capfirst: true})} placeholder={t("filter by productor", { capfirst: true })}
data={productors} data={productors}
defaultValue={defaultProductors} defaultValue={defaultProductors}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'productors') onFilterChange(values, "productors");
}} }}
clearable clearable
searchable searchable
/> />
</Group> </Group>
); );
} }

View File

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

View File

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

View File

@@ -1,73 +1,72 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { ProductType, ProductUnit, type Product } from "@/services/resources/products"; import { ProductType, ProductUnit, type Product } from "@/services/resources/products";
import { useDeleteProduct } from "@/services/api"; import { useDeleteProduct } from "@/services/api";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
export type ProductRowProps = { export type ProductRowProps = {
product: Product; product: Product;
} };
export default function ProductRow({ export default function ProductRow({ product }: ProductRowProps) {
product, const [searchParams] = useSearchParams();
}: ProductRowProps) { const deleteMutation = useDeleteProduct();
const [searchParams] = useSearchParams(); const navigate = useNavigate();
const deleteMutation = useDeleteProduct();
const navigate = useNavigate(); return (
<Table.Tr key={product.id}>
return ( <Table.Td>{product.name}</Table.Td>
<Table.Tr key={product.id}> <Table.Td>{t(ProductType[product.type])}</Table.Td>
<Table.Td>{product.name}</Table.Td> <Table.Td>
<Table.Td>{t(ProductType[product.type])}</Table.Td> {product.price
<Table.Td> ? Intl.NumberFormat("fr-FR", {
{ style: "currency",
product.price ? currency: "EUR",
Intl.NumberFormat( }).format(product.price)
"fr-FR", : null}
{style: "currency", currency: "EUR"} </Table.Td>
).format(product.price) : null <Table.Td>
} {product.price_kg
</Table.Td> ? `${Intl.NumberFormat("fr-FR", {
<Table.Td> style: "currency",
{ currency: "EUR",
product.price_kg ? }).format(product.price_kg)}/kg`
`${Intl.NumberFormat( : null}
"fr-FR", </Table.Td>
{style: "currency", currency: "EUR"} <Table.Td>
).format(product.price_kg)}/kg` : null {product.quantity}
{product.quantity_unit}
} </Table.Td>
</Table.Td> <Table.Td>{t(ProductUnit[product.unit], { capfirst: true })}</Table.Td>
<Table.Td>{product.quantity}{product.quantity_unit}</Table.Td> <Table.Td>
<Table.Td>{t(ProductUnit[product.unit], {capfirst: true})}</Table.Td> <Tooltip label={t("edit product", { capfirst: true })}>
<Table.Td> <ActionIcon
<Tooltip label={t("edit product", {capfirst: true})}> size="sm"
<ActionIcon mr="5"
size="sm" onClick={(e) => {
mr="5" e.stopPropagation();
onClick={(e) => { navigate(
e.stopPropagation(); `/dashboard/products/${product.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
navigate(`/dashboard/products/${product.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`); );
}} }}
> >
<IconEdit/> <IconEdit />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("remove product", {capfirst: true})}> <Tooltip label={t("remove product", { capfirst: true })}>
<ActionIcon <ActionIcon
color="red" color="red"
size="sm" size="sm"
mr="5" mr="5"
onClick={() => { onClick={() => {
deleteMutation.mutate(product.id); deleteMutation.mutate(product.id);
}} }}
> >
<IconX/> <IconX />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
);
); }
}

View File

@@ -1,51 +1,51 @@
import { Group, MultiSelect } from "@mantine/core"; import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react"; import { useMemo } from "react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
export type ShipmentFiltersProps = { export type ShipmentFiltersProps = {
names: string[]; names: string[];
forms: string[]; forms: string[];
filters: URLSearchParams; filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} };
export default function ShipmentsFilters({ export default function ShipmentsFilters({
names, names,
forms, forms,
filters, filters,
onFilterChange onFilterChange,
}: ShipmentFiltersProps) { }: ShipmentFiltersProps) {
const defaultNames = useMemo(() => { const defaultNames = useMemo(() => {
return filters.getAll("names") return filters.getAll("names");
}, [filters]); }, [filters]);
const defaultForms = useMemo(() => { const defaultForms = useMemo(() => {
return filters.getAll("forms") return filters.getAll("forms");
}, [filters]); }, [filters]);
return ( return (
<Group> <Group>
<MultiSelect <MultiSelect
aria-label={t("filter by name", {capfirst: true})} aria-label={t("filter by name", { capfirst: true })}
placeholder={t("filter by name", {capfirst: true})} placeholder={t("filter by name", { capfirst: true })}
data={names} data={names}
defaultValue={defaultNames} defaultValue={defaultNames}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'names') onFilterChange(values, "names");
}} }}
clearable clearable
searchable searchable
/> />
<MultiSelect <MultiSelect
aria-label={t("filter by form", {capfirst: true})} aria-label={t("filter by form", { capfirst: true })}
placeholder={t("filter by form", {capfirst: true})} placeholder={t("filter by form", { capfirst: true })}
data={forms} data={forms}
defaultValue={defaultForms} defaultValue={defaultForms}
onChange={(values: string[]) => { onChange={(values: string[]) => {
onFilterChange(values, 'forms') onFilterChange(values, "forms");
}} }}
clearable clearable
searchable searchable
/> />
</Group> </Group>
); );
} }

View File

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

View File

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

View File

@@ -1,52 +1,52 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { useDeleteShipment} from "@/services/api"; import { useDeleteShipment } from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import type { Shipment } from "@/services/resources/shipments"; import type { Shipment } from "@/services/resources/shipments";
export type ShipmentRowProps = { export type ShipmentRowProps = {
shipment: Shipment; shipment: Shipment;
} };
export default function ShipmentRow({ export default function ShipmentRow({ shipment }: ShipmentRowProps) {
shipment, const [searchParams] = useSearchParams();
}: ShipmentRowProps) { const deleteMutation = useDeleteShipment();
const [searchParams] = useSearchParams(); const navigate = useNavigate();
const deleteMutation = useDeleteShipment();
const navigate = useNavigate(); return (
<Table.Tr key={shipment.id}>
return ( <Table.Td>{shipment.name}</Table.Td>
<Table.Tr key={shipment.id}> <Table.Td>{shipment.date}</Table.Td>
<Table.Td>{shipment.name}</Table.Td> <Table.Td>{`${shipment.form.name} ${shipment.form.season}`}</Table.Td>
<Table.Td>{shipment.date}</Table.Td> <Table.Td>
<Table.Td>{`${shipment.form.name} ${shipment.form.season}`}</Table.Td> <Tooltip label={t("edit productor", { capfirst: true })}>
<Table.Td> <ActionIcon
<Tooltip label={t("edit productor", {capfirst: true})}> size="sm"
<ActionIcon mr="5"
size="sm" onClick={(e) => {
mr="5" e.stopPropagation();
onClick={(e) => { navigate(
e.stopPropagation(); `/dashboard/shipments/${shipment.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
navigate(`/dashboard/shipments/${shipment.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`); );
}} }}
> >
<IconEdit/> <IconEdit />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("remove productor", {capfirst: true})}> <Tooltip label={t("remove productor", { capfirst: true })}>
<ActionIcon <ActionIcon
color="red" color="red"
size="sm" size="sm"
mr="5" mr="5"
onClick={() => { onClick={() => {
deleteMutation.mutate(shipment.id); deleteMutation.mutate(shipment.id);
}} }}
> >
<IconX/> <IconX />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
); );
} }

View File

@@ -1,34 +1,30 @@
import { Group, MultiSelect } from "@mantine/core"; import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react"; import { useMemo } from "react";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
export type UserFiltersProps = { export type UserFiltersProps = {
names: string[]; names: string[];
filters: URLSearchParams; filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void; onFilterChange: (values: string[], filter: string) => void;
} };
export default function UserFilters({ export default function UserFilters({ names, filters, onFilterChange }: UserFiltersProps) {
names, const defaultNames = useMemo(() => {
filters, return filters.getAll("names");
onFilterChange }, [filters]);
}: UserFiltersProps) {
const defaultNames = useMemo(() => { return (
return filters.getAll("names") <Group>
}, [filters]); <MultiSelect
aria-label={t("filter by name", { capfirst: true })}
return ( placeholder={t("filter by name", { capfirst: true })}
<Group> data={names}
<MultiSelect defaultValue={defaultNames}
aria-label={t("filter by name", {capfirst: true})} onChange={(values: string[]) => {
placeholder={t("filter by name", {capfirst: true})} onFilterChange(values, "names");
data={names} }}
defaultValue={defaultNames} clearable
onChange={(values: string[]) => { />
onFilterChange(values, 'names') </Group>
}} );
clearable }
/>
</Group>
);
}

View File

@@ -1,76 +1,77 @@
import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core"; import { Button, Group, Modal, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react"; import { IconCancel } from "@tabler/icons-react";
import { type User, type UserInputs } from "@/services/resources/users"; import { type User, type UserInputs } from "@/services/resources/users";
export type UserModalProps = ModalBaseProps & { export type UserModalProps = ModalBaseProps & {
currentUser?: User; currentUser?: User;
handleSubmit: (user: UserInputs, id?: number) => void; handleSubmit: (user: UserInputs, id?: number) => void;
} };
export function UserModal({ export function UserModal({ opened, onClose, currentUser, handleSubmit }: UserModalProps) {
opened, const form = useForm<UserInputs>({
onClose, initialValues: {
currentUser, name: currentUser?.name ?? "",
handleSubmit email: currentUser?.email ?? "",
}: UserModalProps) { },
const form = useForm<UserInputs>({ validate: {
initialValues: { name: (value) =>
name: currentUser?.name ?? "", !value ? `${t("name", { capfirst: true })} ${t("is required")}` : null,
email: currentUser?.email ?? "" email: (value) =>
}, !value ? `${t("email", { capfirst: true })} ${t("is required")}` : null,
validate: { },
name: (value) => });
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
email: (value) => return (
!value ? `${t("email", {capfirst: true})} ${t('is required')}` : null, <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 })}
return ( placeholder={t("user name", { capfirst: true })}
<Modal radius="sm"
opened={opened} withAsterisk
onClose={onClose} {...form.getInputProps("name")}
title={t("create user", {capfirst: true})} />
> <TextInput
<Title order={4}>{t("informations", {capfirst: true})}</Title> label={t("user email", { capfirst: true })}
<TextInput placeholder={t("user email", { capfirst: true })}
label={t("user name", {capfirst: true})} radius="sm"
placeholder={t("user name", {capfirst: true})} withAsterisk
radius="sm" {...form.getInputProps("email")}
withAsterisk />
{...form.getInputProps('name')} <Group mt="sm" justify="space-between">
/> <Button
<TextInput variant="filled"
label={t("user email", {capfirst: true})} color="red"
placeholder={t("user email", {capfirst: true})} aria-label={t("cancel", { capfirst: true })}
radius="sm" leftSection={<IconCancel />}
withAsterisk onClick={() => {
{...form.getInputProps('email')} form.clearErrors();
/> onClose();
<Group mt="sm" justify="space-between"> }}
<Button >
variant="filled" {t("cancel", { capfirst: true })}
color="red" </Button>
aria-label={t("cancel", {capfirst: true})} <Button
leftSection={<IconCancel/>} variant="filled"
onClick={() => { aria-label={
form.clearErrors(); currentUser
onClose(); ? t("edit user", { capfirst: true })
}} : t("create user", { capfirst: true })
>{t("cancel", {capfirst: true})}</Button> }
<Button onClick={() => {
variant="filled" form.validate();
aria-label={currentUser ? t("edit user", {capfirst: true}) : t('create user', {capfirst: true})} if (form.isValid()) {
onClick={() => { handleSubmit(form.getValues(), currentUser?.id);
form.validate(); }
if (form.isValid()) { }}
handleSubmit(form.getValues(), currentUser?.id) >
} {currentUser
}} ? t("edit user", { capfirst: true })
>{currentUser ? t("edit user", {capfirst: true}) : t('create user', {capfirst: true})}</Button> : t("create user", { capfirst: true })}
</Group> </Button>
</Modal> </Group>
); </Modal>
} );
}

View File

@@ -1,52 +1,51 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react"; import { IconEdit, IconX } from "@tabler/icons-react";
import { type User } from "@/services/resources/users"; import { type User } from "@/services/resources/users";
import { useDeleteUser } from "@/services/api"; import { useDeleteUser } from "@/services/api";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
export type UserRowProps = { export type UserRowProps = {
user: User; user: User;
} };
export default function UserRow({ export default function UserRow({ user }: UserRowProps) {
user, const [searchParams] = useSearchParams();
}: UserRowProps) { const deleteMutation = useDeleteUser();
const [searchParams] = useSearchParams(); const navigate = useNavigate();
const deleteMutation = useDeleteUser();
const navigate = useNavigate(); return (
<Table.Tr key={user.id}>
return ( <Table.Td>{user.name}</Table.Td>
<Table.Tr key={user.id}> <Table.Td>{user.email}</Table.Td>
<Table.Td>{user.name}</Table.Td> <Table.Td>
<Table.Td>{user.email}</Table.Td> <Tooltip label={t("edit user", { capfirst: true })}>
<Table.Td> <ActionIcon
<Tooltip label={t("edit user", {capfirst: true})}> size="sm"
<ActionIcon mr="5"
size="sm" onClick={(e) => {
mr="5" e.stopPropagation();
onClick={(e) => { navigate(
e.stopPropagation(); `/dashboard/users/${user.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`,
navigate(`/dashboard/users/${user.id}/edit${searchParams ? `?${searchParams.toString()}` : ""}`); );
}} }}
> >
<IconEdit/> <IconEdit />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={t("remove user", {capfirst: true})}> <Tooltip label={t("remove user", { capfirst: true })}>
<ActionIcon <ActionIcon
color="red" color="red"
size="sm" size="sm"
mr="5" mr="5"
onClick={() => { onClick={() => {
deleteMutation.mutate(user.id); deleteMutation.mutate(user.id);
}} }}
> >
<IconX/> <IconX />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
);
); }
}

View File

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

View File

@@ -8,46 +8,42 @@ import fr from "@/../locales/fr.json";
import { Config } from "@/config/config"; import { Config } from "@/config/config";
const resources = { const resources = {
en: { translation: en }, en: { translation: en },
fr: { translation: fr }, fr: { translation: fr },
}; };
i18next i18next
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources: resources, resources: resources,
fallbackLng: "fr", fallbackLng: "fr",
debug: Config.debug, debug: Config.debug,
detection: { detection: {
caches: [], caches: [],
}, },
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },
ns: ["translation"], ns: ["translation"],
defaultNS: "translation", defaultNS: "translation",
}) })
.then(() => { .then(() => {
[Settings.defaultLocale] = i18next.language.split("-"); [Settings.defaultLocale] = i18next.language.split("-");
i18next.services.formatter?.add(
"capfirst",
(value) => {
if (typeof value !== "string" || !value.length) {
return value;
}
return value.charAt(0).toUpperCase() + value.slice(1);
}
);
});
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>) { export function t(message: string, params?: Record<string, unknown>) {
const result = i18next.t(message, params); const result = i18next.t(message, params);
if (params?.capfirst && typeof result === "string" && result.length) { if (params?.capfirst && typeof result === "string" && result.length) {
return result.charAt(0).toUpperCase() + result.slice(1); return result.charAt(0).toUpperCase() + result.slice(1);
} }
return result; return result;
} }
export default i18next; export default i18next;

View File

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

View File

@@ -1,268 +1,281 @@
import { ProductForm } from "@/components/Products/Form"; import { ProductForm } from "@/components/Products/Form";
import ShipmentForm from "@/components/Shipments/Form"; import ShipmentForm from "@/components/Shipments/Form";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useCreateContract, useGetForm } from "@/services/api"; import { useCreateContract, useGetForm } from "@/services/api";
import { type Product } from "@/services/resources/products"; import { type Product } from "@/services/resources/products";
import { Accordion, Button, Group, List, Loader, Overlay, Stack, Text, TextInput, Title } from "@mantine/core"; import {
import { useForm } from "@mantine/form"; Accordion,
import { IconMail, IconPhone, IconUser } from "@tabler/icons-react"; Button,
import { useCallback, useMemo, useRef } from "react"; Group,
import { useParams } from "react-router"; List,
import { computePrices } from "./price"; Loader,
Overlay,
export function Contract() { Stack,
const { id } = useParams(); Text,
const { data: form } = useGetForm(Number(id), {enabled: !!id}); TextInput,
const inputForm = useForm<Record<string, number | string>>({ Title,
initialValues: { } from "@mantine/core";
firstname: "", import { useForm } from "@mantine/form";
lastname: "", import { IconMail, IconPhone, IconUser } from "@tabler/icons-react";
email: "", import { useCallback, useMemo, useRef } from "react";
phone: "", import { useParams } from "react-router";
}, import { computePrices } from "./price";
validate: {
firstname: (value) => !value ? `${t("a firstname", {capfirst: true})} ${t("is required")}` : null, export function Contract() {
lastname: (value) => !value ? `${t("a lastname", {capfirst: true})} ${t("is required")}` : null, const { id } = useParams();
email: (value) => !value ? `${t("a email", {capfirst: true})} ${t("is required")}` : null, const { data: form } = useGetForm(Number(id), { enabled: !!id });
phone: (value) => !value ? `${t("a phone", {capfirst: true})} ${t("is required")}` : null, const inputForm = useForm<Record<string, number | string>>({
} initialValues: {
}); firstname: "",
lastname: "",
const createContractMutation = useCreateContract(); email: "",
phone: "",
const productsRecurent = useMemo(() => { },
return form?.productor?.products.filter((el) => el.type === "2") validate: {
}, [form]); firstname: (value) =>
!value ? `${t("a firstname", { capfirst: true })} ${t("is required")}` : null,
const shipments = useMemo(() => { lastname: (value) =>
return form?.shipments; !value ? `${t("a lastname", { capfirst: true })} ${t("is required")}` : null,
}, [form]); email: (value) =>
!value ? `${t("a email", { capfirst: true })} ${t("is required")}` : null,
const allProducts = useMemo(() => { phone: (value) =>
return form?.productor?.products; !value ? `${t("a phone", { capfirst: true })} ${t("is required")}` : null,
}, [form]) },
});
const price = useMemo(() => {
if (!allProducts) { const createContractMutation = useCreateContract();
return 0;
} const productsRecurent = useMemo(() => {
const values = Object.entries(inputForm.getValues()); return form?.productor?.products.filter((el) => el.type === "2");
return computePrices(values, allProducts, form?.shipments.length); }, [form]);
}, [inputForm, allProducts, form?.shipments]);
const shipments = useMemo(() => {
const inputRefs = useRef<Record<string, HTMLInputElement | null>>({ return form?.shipments;
firstname: null, }, [form]);
lastname: null,
email: null, const allProducts = useMemo(() => {
phone: null return form?.productor?.products;
}); }, [form]);
const isShipmentsMinimumValue = useCallback(() => { const price = useMemo(() => {
if (!form) if (!allProducts) {
return false; return 0;
const shipmentErrors = form.shipments }
.map((shipment) => { const values = Object.entries(inputForm.getValues());
const total = computePrices( return computePrices(values, allProducts, form?.shipments.length);
Object.entries(inputForm.getValues()), }, [inputForm, allProducts, form?.shipments]);
shipment.products
); const inputRefs = useRef<Record<string, HTMLInputElement | null>>({
if (total < (form?.minimum_shipment_value || 0)) { firstname: null,
return shipment.id; lastname: null,
} email: null,
return null; phone: null,
}) });
.filter(Boolean);
return shipmentErrors.length === 0; const isShipmentsMinimumValue = useCallback(() => {
}, [form, inputForm]); if (!form) return false;
const shipmentErrors = form.shipments
const withDefaultValues = useCallback((values: Record<string, number | string>) => { .map((shipment) => {
if (!productsRecurent || !form) const total = computePrices(
return {}; Object.entries(inputForm.getValues()),
const result = {...values}; shipment.products,
);
productsRecurent.forEach((product: Product) => { if (total < (form?.minimum_shipment_value || 0)) {
const key = `recurrent-${product.id}`; return shipment.id;
if (result[key] === undefined || result[key] === "") { }
result[key] = 0; return null;
} })
}); .filter(Boolean);
return shipmentErrors.length === 0;
form.shipments.forEach((shipment) => { }, [form, inputForm]);
shipment.products.forEach((product) => {
const key = `planned-${shipment.id}-${product.id}`; const withDefaultValues = useCallback(
if (result[key] === undefined || result[key] === "") { (values: Record<string, number | string>) => {
result[key] = 0; if (!productsRecurent || !form) return {};
} const result = { ...values };
})
}); productsRecurent.forEach((product: Product) => {
const key = `recurrent-${product.id}`;
return result; if (result[key] === undefined || result[key] === "") {
}, [productsRecurent, form]); result[key] = 0;
}
const handleSubmit = useCallback(async () => { });
const errors = inputForm.validate();
if (!form) { form.shipments.forEach((shipment) => {
return; shipment.products.forEach((product) => {
} const key = `planned-${shipment.id}-${product.id}`;
if (inputForm.isValid() && isShipmentsMinimumValue()) { if (result[key] === undefined || result[key] === "") {
const contract = { result[key] = 0;
form_id: form.id, }
contract: withDefaultValues(inputForm.getValues()), });
} });
await createContractMutation.mutateAsync(contract);
} else { return result;
const firstErrorField = Object.keys(errors.errors)[0]; },
const ref = inputRefs.current[firstErrorField]; [productsRecurent, form],
ref?.scrollIntoView({behavior: "smooth", block: "center"}); );
}
}, [inputForm, inputRefs, isShipmentsMinimumValue, form, createContractMutation, withDefaultValues]); const handleSubmit = useCallback(async () => {
const errors = inputForm.validate();
if (!form) {
if (!form || !shipments || !productsRecurent) return;
return ( }
<Group if (inputForm.isValid() && isShipmentsMinimumValue()) {
align="center" const contract = {
justify="center" form_id: form.id,
h="80vh" contract: withDefaultValues(inputForm.getValues()),
w="100%" };
> await createContractMutation.mutateAsync(contract);
<Loader color="pink"/> } else {
</Group> const firstErrorField = Object.keys(errors.errors)[0];
); const ref = inputRefs.current[firstErrorField];
ref?.scrollIntoView({ behavior: "smooth", block: "center" });
}
return ( }, [
<Stack w={{base: "100%", md: "80%", lg: "50%"}}> inputForm,
<Title order={2}>{form.name}</Title> inputRefs,
<Title order={3}>{t("informations", {capfirst: true})}</Title> isShipmentsMinimumValue,
<Text size="sm"> form,
{t("all theses informations are for contract generation", {capfirst: true})} createContractMutation,
</Text> withDefaultValues,
<Group grow> ]);
<TextInput
label={t("firstname", {capfirst: true})} if (!form || !shipments || !productsRecurent)
placeholder={t("firstname", {capfirst: true})} return (
radius="sm" <Group align="center" justify="center" h="80vh" w="100%">
withAsterisk <Loader color="pink" />
required </Group>
leftSection={<IconUser/>} );
{...inputForm.getInputProps('firstname')}
ref={(el) => {inputRefs.current.firstname = el}} return (
/> <Stack w={{ base: "100%", md: "80%", lg: "50%" }}>
<TextInput <Title order={2}>{form.name}</Title>
label={t("lastname", {capfirst: true})} <Title order={3}>{t("informations", { capfirst: true })}</Title>
placeholder={t("lastname", {capfirst: true})} <Text size="sm">
radius="sm" {t("all theses informations are for contract generation", {
withAsterisk capfirst: true,
required })}
leftSection={<IconUser/>} </Text>
{...inputForm.getInputProps('lastname')} <Group grow>
ref={(el) => {inputRefs.current.lastname = el}} <TextInput
/> label={t("firstname", { capfirst: true })}
</Group> placeholder={t("firstname", { capfirst: true })}
<Group grow> radius="sm"
<TextInput withAsterisk
label={t("email", {capfirst: true})} required
placeholder={t("email", {capfirst: true})} leftSection={<IconUser />}
radius="sm" {...inputForm.getInputProps("firstname")}
withAsterisk ref={(el) => {
required inputRefs.current.firstname = el;
leftSection={<IconMail/>} }}
{...inputForm.getInputProps('email')} />
ref={(el) => {inputRefs.current.email = el}} <TextInput
/> label={t("lastname", { capfirst: true })}
<TextInput placeholder={t("lastname", { capfirst: true })}
label={t("phone", {capfirst: true})} radius="sm"
placeholder={t("phone", {capfirst: true})} withAsterisk
radius="sm" required
withAsterisk leftSection={<IconUser />}
required {...inputForm.getInputProps("lastname")}
leftSection={<IconPhone/>} ref={(el) => {
{...inputForm.getInputProps('phone')} inputRefs.current.lastname = el;
ref={(el) => {inputRefs.current.phone = el}} }}
/> />
</Group> </Group>
<Title order={3}>{t('shipments', {capfirst: true})}</Title> <Group grow>
<Text>{`${t("there is", {capfirst: true})} ${shipments.length} ${shipments.length > 1 ? t("shipments") : t("shipment")} ${t("for this contract")}`}</Text> <TextInput
<List> label={t("email", { capfirst: true })}
{ placeholder={t("email", { capfirst: true })}
shipments.map(shipment => ( radius="sm"
<List.Item key={shipment.id}>{`${shipment.name} : withAsterisk
${ required
new Date(shipment.date).toLocaleDateString("fr-FR", { leftSection={<IconMail />}
weekday: "long", {...inputForm.getInputProps("email")}
year: "numeric", ref={(el) => {
month: "long", inputRefs.current.email = el;
day: "numeric", }}
}) />
}`} <TextInput
</List.Item> label={t("phone", { capfirst: true })}
)) placeholder={t("phone", { capfirst: true })}
} radius="sm"
</List> withAsterisk
{ required
productsRecurent.length > 0 ? leftSection={<IconPhone />}
<> {...inputForm.getInputProps("phone")}
<Title order={3}>{t('recurrent products', {capfirst: true})}</Title> ref={(el) => {
<Text size="sm">{t('your selection in this category will apply for all shipments', {capfirst: true})}</Text> inputRefs.current.phone = el;
{ }}
productsRecurent.map((product) => ( />
<ProductForm </Group>
key={product.id} <Title order={3}>{t("shipments", { capfirst: true })}</Title>
product={product} <Text>{`${t("there is", { capfirst: true })} ${shipments.length} ${shipments.length > 1 ? t("shipments") : t("shipment")} ${t("for this contract")}`}</Text>
inputForm={inputForm} <List>
/> {shipments.map((shipment) => (
)) <List.Item key={shipment.id}>
} {`${shipment.name} :
</> : ${new Date(shipment.date).toLocaleDateString("fr-FR", {
null weekday: "long",
} year: "numeric",
{ month: "long",
shipments.some(shipment => shipment.products.length > 0) ? day: "numeric",
<> })}`}
<Title order={3}>{t("planned products")}</Title> </List.Item>
<Text>{t("select products per shipment")}</Text> ))}
<Accordion defaultValue={"0"}> </List>
{ {productsRecurent.length > 0 ? (
shipments.map((shipment, index) => ( <>
<ShipmentForm <Title order={3}>{t("recurrent products", { capfirst: true })}</Title>
minimumPrice={form.minimum_shipment_value} <Text size="sm">
shipment={shipment} {t("your selection in this category will apply for all shipments", {
index={index} capfirst: true,
inputForm={inputForm} })}
key={shipment.id} </Text>
/> {productsRecurent.map((product) => (
)) <ProductForm key={product.id} product={product} inputForm={inputForm} />
} ))}
</Accordion> </>
</> : ) : null}
null {shipments.some((shipment) => shipment.products.length > 0) ? (
} <>
<Overlay <Title order={3}>{t("planned products")}</Title>
bg={"lightGray"} <Text>{t("select products per shipment")}</Text>
h="10vh" <Accordion defaultValue={"0"}>
p="sm" {shipments.map((shipment, index) => (
style={{ <ShipmentForm
display: "flex", minimumPrice={form.minimum_shipment_value}
justifyContent: "space-between", shipment={shipment}
alignItems: "center", index={index}
position: "sticky", inputForm={inputForm}
bottom: "0px", key={shipment.id}
}} />
> ))}
<Text>{ </Accordion>
t("total", {capfirst: true})} : {Intl.NumberFormat( </>
"fr-FR", ) : null}
{style: "currency", currency: "EUR"} <Overlay
).format(price)} bg={"lightGray"}
</Text> h="10vh"
<Button p="sm"
aria-label={t('submit contract')} style={{
onClick={handleSubmit} display: "flex",
> justifyContent: "space-between",
{t('submit contract')} alignItems: "center",
</Button> position: "sticky",
</Overlay> bottom: "0px",
</Stack> }}
) >
} <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>
</Overlay>
</Stack>
);
}

View File

@@ -1,17 +1,23 @@
import type { Product } from "@/services/resources/products"; import type { Product } from "@/services/resources/products";
export function computePrices(values: [string, string | number][], products: Product[], nbShipment?: number) { export function computePrices(
return values.reduce((prev, [key, value]) => { values: [string, string | number][],
const keyArray = key.split("-"); products: Product[],
const productId = Number(keyArray[keyArray.length - 1]); nbShipment?: number,
const product = products.find((product) => product.id === productId); ) {
if (!product) { return values.reduce((prev, [key, value]) => {
return prev + 0; const keyArray = key.split("-");
} const productId = Number(keyArray[keyArray.length - 1]);
const isRecurent = key.includes("recurrent") && nbShipment; const product = products.find((product) => product.id === productId);
const productPrice = Number(product.price || product.price_kg); if (!product) {
const productQuantityUnit = product.unit === "2" ? 1 : 1000; return prev + 0;
const productQuantity = Number(product.price ? Number(value) : Number(value) / productQuantityUnit); }
return(prev + productPrice * productQuantity * (isRecurent ? nbShipment : 1)); const isRecurent = key.includes("recurrent") && nbShipment;
}, 0); 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);
}, 0);
}

View File

@@ -1,28 +1,28 @@
import { Tabs } from "@mantine/core"; import { Tabs } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { Outlet, useLocation, useNavigate } from "react-router"; import { Outlet, useLocation, useNavigate } from "react-router";
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
return ( return (
<Tabs <Tabs
w={{base: "100%", md: "80%", lg: "60%"}} w={{ base: "100%", md: "80%", lg: "60%" }}
orientation={"horizontal"} orientation={"horizontal"}
value={location.pathname.split('/')[2]} value={location.pathname.split("/")[2]}
defaultValue={"productors"} defaultValue={"productors"}
onChange={(value) => navigate(`/dashboard/${value}`)} onChange={(value) => navigate(`/dashboard/${value}`)}
> >
<Tabs.List> <Tabs.List>
<Tabs.Tab value="productors">{t("productors", {capfirst: true})}</Tabs.Tab> <Tabs.Tab value="productors">{t("productors", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="products">{t("products", {capfirst: true})}</Tabs.Tab> <Tabs.Tab value="products">{t("products", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="forms">{t("forms", {capfirst: true})}</Tabs.Tab> <Tabs.Tab value="forms">{t("forms", { capfirst: true })}</Tabs.Tab>
<Tabs.Tab value="shipments">{t("shipments", {capfirst: true})}</Tabs.Tab> <Tabs.Tab value="shipments">{t("shipments", { capfirst: true })}</Tabs.Tab>
{/* <Tabs.Tab value="templates">{t("templates", {capfirst: true})}</Tabs.Tab> */} {/* <Tabs.Tab value="templates">{t("templates", {capfirst: true})}</Tabs.Tab> */}
<Tabs.Tab value="users">{t("users", {capfirst: true})}</Tabs.Tab> <Tabs.Tab value="users">{t("users", { capfirst: true })}</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Outlet/> <Outlet />
</Tabs> </Tabs>
); );
} }

View File

@@ -1,170 +1,169 @@
import { Stack, Loader, Title, Group, ActionIcon, Tooltip, Table, ScrollArea } from "@mantine/core"; import { Stack, Loader, Title, Group, ActionIcon, Tooltip, Table, ScrollArea } from "@mantine/core";
import { useCreateForm, useEditForm, useGetForm, useGetForms } from "@/services/api"; import { useCreateForm, useEditForm, useGetForm, useGetForms } from "@/services/api";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { IconPlus } from "@tabler/icons-react"; import { IconPlus } from "@tabler/icons-react";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import FormModal from "@/components/Forms/Modal"; import FormModal from "@/components/Forms/Modal";
import FormRow from "@/components/Forms/Row"; import FormRow from "@/components/Forms/Row";
import type { Form, FormInputs } from "@/services/resources/forms"; import type { Form, FormInputs } from "@/services/resources/forms";
import FilterForms from "@/components/Forms/Filter"; import FilterForms from "@/components/Forms/Filter";
export function Forms() { export function Forms() {
const [ searchParams, setSearchParams ] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const isCreate = location.pathname === "/dashboard/forms/create"; const isCreate = location.pathname === "/dashboard/forms/create";
const isEdit = location.pathname.includes("/edit"); const isEdit = location.pathname.includes("/edit");
const editId = useMemo(() => { const editId = useMemo(() => {
if (isEdit) { if (isEdit) {
return location.pathname.split("/")[3] return location.pathname.split("/")[3];
} }
return null return null;
}, [location, isEdit]) }, [location, isEdit]);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
navigate(`/dashboard/forms${searchParams ? `?${searchParams.toString()}` : ""}`); navigate(`/dashboard/forms${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]); }, [navigate, searchParams]);
const { isPending, data } = useGetForms(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(() => { const { data: allForms } = useGetForms();
return allForms?.map((form: Form) => (form.season))
.filter((season, index, array) => array.indexOf(season) === index) const seasons = useMemo(() => {
}, [allForms]) return allForms
?.map((form: Form) => form.season)
const productors = useMemo(() => { .filter((season, index, array) => array.indexOf(season) === index);
return allForms?.map((form: Form) => (form.productor.name)) }, [allForms]);
.filter((productor, index, array) => array.indexOf(productor) === index)
}, [allForms]) const productors = useMemo(() => {
return allForms
const createFormMutation = useCreateForm(); ?.map((form: Form) => form.productor.name)
const editFormMutation = useEditForm(); .filter((productor, index, array) => array.indexOf(productor) === index);
}, [allForms]);
const handleCreateForm = useCallback(async (form: FormInputs) => {
if (!form.start || !form.end) const createFormMutation = useCreateForm();
return; const editFormMutation = useEditForm();
await createFormMutation.mutateAsync({
...form, const handleCreateForm = useCallback(
start: form?.start, async (form: FormInputs) => {
end: form?.end, if (!form.start || !form.end) return;
productor_id: Number(form.productor_id), await createFormMutation.mutateAsync({
referer_id: Number(form.referer_id), ...form,
minimum_shipment_value: Number(form.minimum_shipment_value), start: form?.start,
}); end: form?.end,
closeModal(); productor_id: Number(form.productor_id),
}, [createFormMutation, closeModal]); referer_id: Number(form.referer_id),
minimum_shipment_value: Number(form.minimum_shipment_value),
const handleEditForm = useCallback(async (form: FormInputs, id?: number) => { });
if (!id) closeModal();
return; },
await editFormMutation.mutateAsync({ [createFormMutation, closeModal],
id: id, );
form: {
...form, const handleEditForm = useCallback(
start: form.start, async (form: FormInputs, id?: number) => {
end: form.end, if (!id) return;
productor_id: Number(form.productor_id), await editFormMutation.mutateAsync({
referer_id: Number(form.referer_id), id: id,
minimum_shipment_value: Number(form.minimum_shipment_value), form: {
} ...form,
}); start: form.start,
closeModal(); end: form.end,
}, [editFormMutation, closeModal]); productor_id: Number(form.productor_id),
referer_id: Number(form.referer_id),
const onFilterChange = useCallback(( minimum_shipment_value: Number(form.minimum_shipment_value),
values: string[], },
filter: string });
) => { closeModal();
setSearchParams(prev => { },
const params = new URLSearchParams(prev); [editFormMutation, closeModal],
params.delete(filter) );
values.forEach(value => { const onFilterChange = useCallback(
params.append(filter, value); (values: string[], filter: string) => {
}); setSearchParams((prev) => {
const params = new URLSearchParams(prev);
return params; params.delete(filter);
});
}, [setSearchParams]) values.forEach((value) => {
params.append(filter, value);
});
if (!data || isPending)
return ( return params;
<Group });
align="center" },
justify="center" [setSearchParams],
h="80vh" );
w="100%"
> if (!data || isPending)
<Loader color="pink"/> return (
</Group> <Group align="center" justify="center" h="80vh" w="100%">
); <Loader color="pink" />
</Group>
return ( );
<Stack>
<Group justify="space-between"> return (
<Title order={2}>{t("all forms", {capfirst: true})}</Title> <Stack>
<Tooltip label={t("create new form", {capfirst: true})}> <Group justify="space-between">
<ActionIcon <Title order={2}>{t("all forms", { capfirst: true })}</Title>
onClick={(e) => { <Tooltip label={t("create new form", { capfirst: true })}>
e.stopPropagation(); <ActionIcon
navigate(`/dashboard/forms/create${searchParams ? `?${searchParams.toString()}` : ""}`); onClick={(e) => {
}} e.stopPropagation();
> navigate(
<IconPlus/> `/dashboard/forms/create${searchParams ? `?${searchParams.toString()}` : ""}`,
</ActionIcon> );
</Tooltip> }}
<FormModal >
key={`${currentForm?.id}_create`} <IconPlus />
opened={isCreate} </ActionIcon>
onClose={closeModal} </Tooltip>
handleSubmit={handleCreateForm} <FormModal
/> key={`${currentForm?.id}_create`}
</Group> opened={isCreate}
<FilterForms onClose={closeModal}
productors={productors || []} handleSubmit={handleCreateForm}
seasons={seasons || []} />
filters={searchParams} </Group>
onFilterChange={onFilterChange} <FilterForms
/> productors={productors || []}
<FormModal seasons={seasons || []}
key={`${currentForm?.id}_edit`} filters={searchParams}
opened={isEdit} onFilterChange={onFilterChange}
onClose={closeModal} />
currentForm={currentForm} <FormModal
handleSubmit={handleEditForm} key={`${currentForm?.id}_edit`}
/> opened={isEdit}
<ScrollArea type="auto"> onClose={closeModal}
<Table striped> currentForm={currentForm}
<Table.Thead> handleSubmit={handleEditForm}
<Table.Tr> />
<Table.Th>{t("name", {capfirst: true})}</Table.Th> <ScrollArea type="auto">
<Table.Th>{t("type", {capfirst: true})}</Table.Th> <Table striped>
<Table.Th>{t("start", {capfirst: true})}</Table.Th> <Table.Thead>
<Table.Th>{t("end", {capfirst: true})}</Table.Th> <Table.Tr>
<Table.Th>{t("productor", {capfirst: true})}</Table.Th> <Table.Th>{t("name", { capfirst: true })}</Table.Th>
<Table.Th>{t("referer", {capfirst: true})}</Table.Th> <Table.Th>{t("type", { capfirst: true })}</Table.Th>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th> <Table.Th>{t("start", { capfirst: true })}</Table.Th>
</Table.Tr> <Table.Th>{t("end", { capfirst: true })}</Table.Th>
</Table.Thead> <Table.Th>{t("productor", { capfirst: true })}</Table.Th>
<Table.Tbody> <Table.Th>{t("referer", { capfirst: true })}</Table.Th>
{ <Table.Th>{t("actions", { capfirst: true })}</Table.Th>
data.map((form) => ( </Table.Tr>
<FormRow </Table.Thead>
form={form} <Table.Tbody>
key={form.id} {data.map((form) => (
/> <FormRow form={form} key={form.id} />
)) ))}
} </Table.Tbody>
</Table.Tbody> </Table>
</Table> </ScrollArea>
</ScrollArea> </Stack>
</Stack> );
); }
}

View File

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

View File

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

View File

@@ -1,148 +1,157 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core"; import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useCreateProductor, useEditProductor, useGetProductor, useGetProductors } from "@/services/api"; import {
import { IconPlus } from "@tabler/icons-react"; useCreateProductor,
import ProductorRow from "@/components/Productors/Row"; useEditProductor,
import { useLocation, useNavigate, useSearchParams } from "react-router"; useGetProductor,
import { ProductorModal } from "@/components/Productors/Modal"; useGetProductors,
import { useCallback, useMemo } from "react"; } from "@/services/api";
import type { Productor, ProductorInputs } from "@/services/resources/productors"; import { IconPlus } from "@tabler/icons-react";
import ProductorsFilters from "@/components/Productors/Filter"; import ProductorRow from "@/components/Productors/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
export default function Productors() { import { ProductorModal } from "@/components/Productors/Modal";
const [ searchParams, setSearchParams ] = useSearchParams(); import { useCallback, useMemo } from "react";
const location = useLocation(); import type { Productor, ProductorInputs } from "@/services/resources/productors";
const navigate = useNavigate(); import ProductorsFilters from "@/components/Productors/Filter";
const isCreate = location.pathname === "/dashboard/productors/create"; export default function Productors() {
const isEdit = location.pathname.includes("/edit"); const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const editId = useMemo(() => { const navigate = useNavigate();
if (isEdit) {
return location.pathname.split("/")[3] const isCreate = location.pathname === "/dashboard/productors/create";
} const isEdit = location.pathname.includes("/edit");
return null
}, [location, isEdit]) const editId = useMemo(() => {
if (isEdit) {
const closeModal = useCallback(() => { return location.pathname.split("/")[3];
navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`); }
}, [navigate, searchParams]); return null;
}, [location, isEdit]);
const { data: productors, isPending } = useGetProductors(searchParams);
const { data: currentProductor } = useGetProductor(Number(editId), { enabled: !!editId }); const closeModal = useCallback(() => {
const { data: allProductors } = useGetProductors(); navigate(`/dashboard/productors${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]);
const names = useMemo(() => {
return allProductors?.map((productor: Productor) => (productor.name)) const { data: productors, isPending } = useGetProductors(searchParams);
.filter((season, index, array) => array.indexOf(season) === index) const { data: currentProductor } = useGetProductor(Number(editId), {
}, [allProductors]) enabled: !!editId,
});
const types = useMemo(() => { const { data: allProductors } = useGetProductors();
return allProductors?.map((productor: Productor) => (productor.type))
.filter((productor, index, array) => array.indexOf(productor) === index) const names = useMemo(() => {
}, [allProductors]) return allProductors
?.map((productor: Productor) => productor.name)
const createProductorMutation = useCreateProductor(); .filter((season, index, array) => array.indexOf(season) === index);
const editProductorMutation = useEditProductor(); }, [allProductors]);
const handleCreateProductor = useCallback(async (productor: ProductorInputs) => { const types = useMemo(() => {
await createProductorMutation.mutateAsync({ return allProductors
...productor ?.map((productor: Productor) => productor.type)
}); .filter((productor, index, array) => array.indexOf(productor) === index);
closeModal(); }, [allProductors]);
}, [createProductorMutation, closeModal]);
const createProductorMutation = useCreateProductor();
const handleEditProductor = useCallback(async (productor: ProductorInputs, id?: number) => { const editProductorMutation = useEditProductor();
if (!id)
return; const handleCreateProductor = useCallback(
await editProductorMutation.mutateAsync({ async (productor: ProductorInputs) => {
id: id, await createProductorMutation.mutateAsync({
productor: productor ...productor,
}); });
closeModal(); closeModal();
}, [editProductorMutation, closeModal]); },
[createProductorMutation, closeModal],
const onFilterChange = useCallback((values: string[], filter: string) => { );
setSearchParams(prev => {
const params = new URLSearchParams(prev); const handleEditProductor = useCallback(
params.delete(filter) async (productor: ProductorInputs, id?: number) => {
if (!id) return;
values.forEach(value => { await editProductorMutation.mutateAsync({
params.append(filter, value); id: id,
}); productor: productor,
return params; });
}); closeModal();
}, [setSearchParams]) },
[editProductorMutation, closeModal],
if (!productors || isPending) );
return (
<Group const onFilterChange = useCallback(
align="center" (values: string[], filter: string) => {
justify="center" setSearchParams((prev) => {
h="80vh" const params = new URLSearchParams(prev);
w="100%" params.delete(filter);
>
<Loader color="pink"/> values.forEach((value) => {
</Group> params.append(filter, value);
); });
return params;
return ( });
<Stack> },
<Group justify="space-between"> [setSearchParams],
<Title order={2}>{t("all productors", {capfirst: true})}</Title> );
<Tooltip label={t("create productor", {capfirst: true})}>
<ActionIcon if (!productors || isPending)
onClick={(e) => { return (
e.stopPropagation(); <Group align="center" justify="center" h="80vh" w="100%">
navigate(`/dashboard/productors/create${searchParams ? `?${searchParams.toString()}` : ""}`); <Loader color="pink" />
}} </Group>
> );
<IconPlus/>
</ActionIcon> return (
</Tooltip> <Stack>
<ProductorModal <Group justify="space-between">
key={`${currentProductor?.id}_create`} <Title order={2}>{t("all productors", { capfirst: true })}</Title>
opened={isCreate} <Tooltip label={t("create productor", { capfirst: true })}>
onClose={closeModal} <ActionIcon
handleSubmit={handleCreateProductor} onClick={(e) => {
/> e.stopPropagation();
<ProductorModal navigate(
key={`${currentProductor?.id}_edit`} `/dashboard/productors/create${searchParams ? `?${searchParams.toString()}` : ""}`,
opened={isEdit} );
onClose={closeModal} }}
currentProductor={currentProductor} >
handleSubmit={handleEditProductor} <IconPlus />
/> </ActionIcon>
</Group> </Tooltip>
<ProductorsFilters <ProductorModal
names={names || []} key={`${currentProductor?.id}_create`}
types={types || []} opened={isCreate}
filters={searchParams} onClose={closeModal}
onFilterChange={onFilterChange} handleSubmit={handleCreateProductor}
/> />
<ScrollArea type="auto"> <ProductorModal
<Table striped> key={`${currentProductor?.id}_edit`}
<Table.Thead> opened={isEdit}
<Table.Tr> onClose={closeModal}
<Table.Th>{t("name", {capfirst: true})}</Table.Th> currentProductor={currentProductor}
<Table.Th>{t("type", {capfirst: true})}</Table.Th> handleSubmit={handleEditProductor}
<Table.Th>{t("address", {capfirst: true})}</Table.Th> />
<Table.Th>{t("payment methods", {capfirst: true})}</Table.Th> </Group>
<Table.Th>{t("actions", {capfirst: true})}</Table.Th> <ProductorsFilters
</Table.Tr> names={names || []}
</Table.Thead> types={types || []}
<Table.Tbody> filters={searchParams}
{ onFilterChange={onFilterChange}
productors.map((productor) => ( />
<ProductorRow <ScrollArea type="auto">
productor={productor} <Table striped>
key={productor.id} <Table.Thead>
/> <Table.Tr>
)) <Table.Th>{t("name", { capfirst: true })}</Table.Th>
} <Table.Th>{t("type", { capfirst: true })}</Table.Th>
</Table.Tbody> <Table.Th>{t("address", { capfirst: true })}</Table.Th>
</Table> <Table.Th>{t("payment methods", { capfirst: true })}</Table.Th>
</ScrollArea> <Table.Th>{t("actions", { capfirst: true })}</Table.Th>
</Stack> </Table.Tr>
); </Table.Thead>
} <Table.Tbody>
{productors.map((productor) => (
<ProductorRow productor={productor} key={productor.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Stack>
);
}

View File

@@ -1,148 +1,156 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core"; import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useCreateProduct, useEditProduct, useGetProduct, useGetProducts } from "@/services/api"; import { useCreateProduct, useEditProduct, useGetProduct, useGetProducts } from "@/services/api";
import { IconPlus } from "@tabler/icons-react"; import { IconPlus } from "@tabler/icons-react";
import ProductRow from "@/components/Products/Row"; import ProductRow from "@/components/Products/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { ProductModal } from "@/components/Products/Modal"; import { ProductModal } from "@/components/Products/Modal";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { productCreateFromProductInputs, type Product, type ProductInputs } from "@/services/resources/products"; import {
import ProductsFilters from "@/components/Products/Filter"; productCreateFromProductInputs,
type Product,
export default function Products() { type ProductInputs,
const [ searchParams, setSearchParams ] = useSearchParams(); } from "@/services/resources/products";
const location = useLocation(); import ProductsFilters from "@/components/Products/Filter";
const navigate = useNavigate();
export default function Products() {
const isCreate = location.pathname === "/dashboard/products/create"; const [searchParams, setSearchParams] = useSearchParams();
const isEdit = location.pathname.includes("/edit"); const location = useLocation();
const navigate = useNavigate();
const editId = useMemo(() => {
if (isEdit) { const isCreate = location.pathname === "/dashboard/products/create";
return location.pathname.split("/")[3] const isEdit = location.pathname.includes("/edit");
}
return null const editId = useMemo(() => {
}, [location, isEdit]) if (isEdit) {
return location.pathname.split("/")[3];
const closeModal = useCallback(() => { }
navigate(`/dashboard/products${searchParams ? `?${searchParams.toString()}` : ""}`); return null;
}, [navigate, searchParams]); }, [location, isEdit]);
const { data: products, isPending } = useGetProducts(searchParams); const closeModal = useCallback(() => {
const { data: currentProduct } = useGetProduct(Number(editId), { enabled: !!editId }); navigate(`/dashboard/products${searchParams ? `?${searchParams.toString()}` : ""}`);
const { data: allProducts } = useGetProducts(); }, [navigate, searchParams]);
const names = useMemo(() => { const { data: products, isPending } = useGetProducts(searchParams);
return allProducts?.map((product: Product) => (product.name)) const { data: currentProduct } = useGetProduct(Number(editId), {
.filter((season, index, array) => array.indexOf(season) === index) enabled: !!editId,
}, [allProducts]) });
const { data: allProducts } = useGetProducts();
const productors = useMemo(() => {
return allProducts?.map((product: Product) => (product.productor.name)) const names = useMemo(() => {
.filter((productor, index, array) => array.indexOf(productor) === index) return allProducts
}, [allProducts]) ?.map((product: Product) => product.name)
.filter((season, index, array) => array.indexOf(season) === index);
const createProductMutation = useCreateProduct(); }, [allProducts]);
const editProductMutation = useEditProduct();
const productors = useMemo(() => {
const handleCreateProduct = useCallback(async (product: ProductInputs) => { return allProducts
await createProductMutation.mutateAsync(productCreateFromProductInputs(product)); ?.map((product: Product) => product.productor.name)
closeModal(); .filter((productor, index, array) => array.indexOf(productor) === index);
}, [createProductMutation, closeModal]); }, [allProducts]);
const handleEditProduct = useCallback(async (product: ProductInputs, id?: number) => { const createProductMutation = useCreateProduct();
if (!id) const editProductMutation = useEditProduct();
return;
await editProductMutation.mutateAsync({ const handleCreateProduct = useCallback(
id: id, async (product: ProductInputs) => {
product: productCreateFromProductInputs(product) await createProductMutation.mutateAsync(productCreateFromProductInputs(product));
}); closeModal();
closeModal(); },
}, [editProductMutation, closeModal]); [createProductMutation, closeModal],
);
const onFilterChange = useCallback((values: string[], filter: string) => {
setSearchParams(prev => { const handleEditProduct = useCallback(
const params = new URLSearchParams(prev); async (product: ProductInputs, id?: number) => {
params.delete(filter); if (!id) return;
await editProductMutation.mutateAsync({
values.forEach(value => { id: id,
params.append(filter, value); product: productCreateFromProductInputs(product),
}); });
return params; closeModal();
}); },
}, [setSearchParams]) [editProductMutation, closeModal],
);
if (!products || isPending)
return ( const onFilterChange = useCallback(
<Group (values: string[], filter: string) => {
align="center" setSearchParams((prev) => {
justify="center" const params = new URLSearchParams(prev);
h="80vh" params.delete(filter);
w="100%"
> values.forEach((value) => {
<Loader color="pink"/> params.append(filter, value);
</Group> });
); return params;
});
return ( },
<Stack> [setSearchParams],
<Group justify="space-between"> );
<Title order={2}>{t("all products", {capfirst: true})}</Title>
<Tooltip label={t("create product", {capfirst: true})}> if (!products || isPending)
<ActionIcon return (
onClick={(e) => { <Group align="center" justify="center" h="80vh" w="100%">
e.stopPropagation(); <Loader color="pink" />
navigate(`/dashboard/products/create${searchParams ? `?${searchParams.toString()}` : ""}`); </Group>
}} );
>
<IconPlus/> return (
</ActionIcon> <Stack>
</Tooltip> <Group justify="space-between">
<ProductModal <Title order={2}>{t("all products", { capfirst: true })}</Title>
key={`${currentProduct?.id}_create`} <Tooltip label={t("create product", { capfirst: true })}>
opened={isCreate} <ActionIcon
onClose={closeModal} onClick={(e) => {
handleSubmit={handleCreateProduct} e.stopPropagation();
/> navigate(
</Group> `/dashboard/products/create${searchParams ? `?${searchParams.toString()}` : ""}`,
<ProductsFilters );
productors = {productors || []} }}
names={names || []} >
filters={searchParams} <IconPlus />
onFilterChange={onFilterChange} </ActionIcon>
/> </Tooltip>
<ProductModal <ProductModal
key={`${currentProduct?.id}_edit`} key={`${currentProduct?.id}_create`}
opened={isEdit} opened={isCreate}
onClose={closeModal} onClose={closeModal}
currentProduct={currentProduct} handleSubmit={handleCreateProduct}
handleSubmit={handleEditProduct} />
/> </Group>
<ScrollArea type="auto"> <ProductsFilters
<Table striped> productors={productors || []}
<Table.Thead> names={names || []}
<Table.Tr> filters={searchParams}
<Table.Th>{t("name", {capfirst: true})}</Table.Th> onFilterChange={onFilterChange}
<Table.Th>{t("type", {capfirst: true})}</Table.Th> />
<Table.Th>{t("price", {capfirst: true})}</Table.Th> <ProductModal
<Table.Th>{t("priceKg", {capfirst: true})}</Table.Th> key={`${currentProduct?.id}_edit`}
<Table.Th>{t("quantity", {capfirst: true})}</Table.Th> opened={isEdit}
<Table.Th>{t("unit", {capfirst: true})}</Table.Th> onClose={closeModal}
<Table.Th>{t("actions", {capfirst: true})}</Table.Th> currentProduct={currentProduct}
</Table.Tr> handleSubmit={handleEditProduct}
</Table.Thead> />
<Table.Tbody> <ScrollArea type="auto">
{ <Table striped>
products.map((product) => ( <Table.Thead>
<ProductRow <Table.Tr>
product={product} <Table.Th>{t("name", { capfirst: true })}</Table.Th>
key={product.id} <Table.Th>{t("type", { capfirst: true })}</Table.Th>
/> <Table.Th>{t("price", { capfirst: true })}</Table.Th>
)) <Table.Th>{t("priceKg", { capfirst: true })}</Table.Th>
} <Table.Th>{t("quantity", { capfirst: true })}</Table.Th>
</Table.Tbody> <Table.Th>{t("unit", { capfirst: true })}</Table.Th>
</Table> <Table.Th>{t("actions", { capfirst: true })}</Table.Th>
</ScrollArea> </Table.Tr>
</Stack> </Table.Thead>
); <Table.Tbody>
} {products.map((product) => (
<ProductRow product={product} key={product.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Stack>
);
}

View File

@@ -1,145 +1,158 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core"; import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useCreateShipment, useEditShipment, useGetShipment, useGetShipments } from "@/services/api"; import {
import { IconPlus } from "@tabler/icons-react"; useCreateShipment,
import ShipmentRow from "@/components/Shipments/Row"; useEditShipment,
import { useLocation, useNavigate, useSearchParams } from "react-router"; useGetShipment,
import { useCallback, useMemo } from "react"; useGetShipments,
import { shipmentCreateFromShipmentInputs, type Shipment, type ShipmentInputs } from "@/services/resources/shipments"; } from "@/services/api";
import ShipmentModal from "@/components/Shipments/Modal"; import { IconPlus } from "@tabler/icons-react";
import ShipmentsFilters from "@/components/Shipments/Filter"; import ShipmentRow from "@/components/Shipments/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router";
export default function Shipments() { import { useCallback, useMemo } from "react";
const [ searchParams, setSearchParams ] = useSearchParams(); import {
const location = useLocation(); shipmentCreateFromShipmentInputs,
const navigate = useNavigate(); type Shipment,
type ShipmentInputs,
const isCreate = location.pathname === "/dashboard/shipments/create"; } from "@/services/resources/shipments";
const isEdit = location.pathname.includes("/edit"); import ShipmentModal from "@/components/Shipments/Modal";
import ShipmentsFilters from "@/components/Shipments/Filter";
const editId = useMemo(() => {
if (isEdit) { export default function Shipments() {
return location.pathname.split("/")[3] const [searchParams, setSearchParams] = useSearchParams();
} const location = useLocation();
return null const navigate = useNavigate();
}, [location, isEdit])
const isCreate = location.pathname === "/dashboard/shipments/create";
const closeModal = useCallback(() => { const isEdit = location.pathname.includes("/edit");
navigate(`/dashboard/shipments${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]); const editId = useMemo(() => {
if (isEdit) {
const { data: shipments, isPending } = useGetShipments(searchParams); return location.pathname.split("/")[3];
const { data: currentShipment } = useGetShipment(Number(editId), { enabled: !!editId }); }
const { data: allShipments } = useGetShipments(); return null;
}, [location, isEdit]);
const names = useMemo(() => {
return allShipments?.map((shipment: Shipment) => (shipment.name)) const closeModal = useCallback(() => {
.filter((season, index, array) => array.indexOf(season) === index) navigate(`/dashboard/shipments${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [allShipments]) }, [navigate, searchParams]);
const forms = useMemo(() => { const { data: shipments, isPending } = useGetShipments(searchParams);
return allShipments?.map((shipment: Shipment) => (shipment.form.name)) const { data: currentShipment } = useGetShipment(Number(editId), {
.filter((season, index, array) => array.indexOf(season) === index) enabled: !!editId,
}, [allShipments]) });
const { data: allShipments } = useGetShipments();
const createShipmentMutation = useCreateShipment();
const editShipmentMutation = useEditShipment(); const names = useMemo(() => {
return allShipments
const handleCreateShipment = useCallback(async (shipment: ShipmentInputs) => { ?.map((shipment: Shipment) => shipment.name)
await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment)); .filter((season, index, array) => array.indexOf(season) === index);
closeModal(); }, [allShipments]);
}, [createShipmentMutation, closeModal]);
const forms = useMemo(() => {
const handleEditShipment = useCallback(async (shipment: ShipmentInputs, id?: number) => { return allShipments
if (!id) ?.map((shipment: Shipment) => shipment.form.name)
return; .filter((season, index, array) => array.indexOf(season) === index);
await editShipmentMutation.mutateAsync({ }, [allShipments]);
id: id,
shipment: shipmentCreateFromShipmentInputs(shipment) const createShipmentMutation = useCreateShipment();
}); const editShipmentMutation = useEditShipment();
closeModal();
}, [editShipmentMutation, closeModal]); const handleCreateShipment = useCallback(
async (shipment: ShipmentInputs) => {
const onFilterChange = useCallback((values: string[], filter: string) => { await createShipmentMutation.mutateAsync(shipmentCreateFromShipmentInputs(shipment));
setSearchParams(prev => { closeModal();
const params = new URLSearchParams(prev); },
params.delete(filter) [createShipmentMutation, closeModal],
);
values.forEach(value => {
params.append(filter, value); const handleEditShipment = useCallback(
}); async (shipment: ShipmentInputs, id?: number) => {
return params; if (!id) return;
}); await editShipmentMutation.mutateAsync({
}, [setSearchParams]) id: id,
shipment: shipmentCreateFromShipmentInputs(shipment),
if (!shipments || isPending) });
return ( closeModal();
<Group },
align="center" [editShipmentMutation, closeModal],
justify="center" );
h="80vh"
w="100%" const onFilterChange = useCallback(
> (values: string[], filter: string) => {
<Loader color="pink"/> setSearchParams((prev) => {
</Group> const params = new URLSearchParams(prev);
); params.delete(filter);
return ( values.forEach((value) => {
<Stack> params.append(filter, value);
<Group justify="space-between"> });
<Title order={2}>{t("all shipments", {capfirst: true})}</Title> return params;
<Tooltip label={t("create shipment", {capfirst: true})}> });
<ActionIcon },
onClick={(e) => { [setSearchParams],
e.stopPropagation(); );
navigate(`/dashboard/shipments/create${searchParams ? `?${searchParams.toString()}` : ""}`);
}} if (!shipments || isPending)
> return (
<IconPlus/> <Group align="center" justify="center" h="80vh" w="100%">
</ActionIcon> <Loader color="pink" />
</Tooltip> </Group>
<ShipmentModal );
key={`${currentShipment?.id}_create`}
opened={isCreate} return (
onClose={closeModal} <Stack>
handleSubmit={handleCreateShipment} <Group justify="space-between">
/> <Title order={2}>{t("all shipments", { capfirst: true })}</Title>
<ShipmentModal <Tooltip label={t("create shipment", { capfirst: true })}>
key={`${currentShipment?.id}_edit`} <ActionIcon
opened={isEdit} onClick={(e) => {
onClose={closeModal} e.stopPropagation();
currentShipment={currentShipment} navigate(
handleSubmit={handleEditShipment} `/dashboard/shipments/create${searchParams ? `?${searchParams.toString()}` : ""}`,
/> );
</Group> }}
<ShipmentsFilters >
forms={forms || []} <IconPlus />
names={names || []} </ActionIcon>
filters={searchParams} </Tooltip>
onFilterChange={onFilterChange} <ShipmentModal
/> key={`${currentShipment?.id}_create`}
<ScrollArea type="auto"> opened={isCreate}
<Table striped> onClose={closeModal}
<Table.Thead> handleSubmit={handleCreateShipment}
<Table.Tr> />
<Table.Th>{t("name", {capfirst: true})}</Table.Th> <ShipmentModal
<Table.Th>{t("date", {capfirst: true})}</Table.Th> key={`${currentShipment?.id}_edit`}
<Table.Th>{t("formulare", {capfirst: true})}</Table.Th> opened={isEdit}
<Table.Th>{t("actions", {capfirst: true})}</Table.Th> onClose={closeModal}
</Table.Tr> currentShipment={currentShipment}
</Table.Thead> handleSubmit={handleEditShipment}
<Table.Tbody> />
{ </Group>
shipments.map((shipment) => ( <ShipmentsFilters
<ShipmentRow forms={forms || []}
shipment={shipment} names={names || []}
key={shipment.id} filters={searchParams}
/> onFilterChange={onFilterChange}
)) />
} <ScrollArea type="auto">
</Table.Tbody> <Table striped>
</Table> <Table.Thead>
</ScrollArea> <Table.Tr>
</Stack> <Table.Th>{t("name", { capfirst: true })}</Table.Th>
); <Table.Th>{t("date", { capfirst: true })}</Table.Th>
} <Table.Th>{t("formulare", { capfirst: true })}</Table.Th>
<Table.Th>{t("actions", { capfirst: true })}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{shipments.map((shipment) => (
<ShipmentRow shipment={shipment} key={shipment.id} />
))}
</Table.Tbody>
</Table>
</ScrollArea>
</Stack>
);
}

View File

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

View File

@@ -1,139 +1,142 @@
import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core"; import { ActionIcon, Group, Loader, ScrollArea, Stack, Table, Title, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
import { useCreateUser, useEditUser, useGetUser, useGetUsers } from "@/services/api"; import { useCreateUser, useEditUser, useGetUser, useGetUsers } from "@/services/api";
import { IconPlus } from "@tabler/icons-react"; import { IconPlus } from "@tabler/icons-react";
import UserRow from "@/components/Users/Row"; import UserRow from "@/components/Users/Row";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate, useSearchParams } from "react-router";
import { UserModal } from "@/components/Users/Modal"; import { UserModal } from "@/components/Users/Modal";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { type User, type UserInputs } from "@/services/resources/users"; import { type User, type UserInputs } from "@/services/resources/users";
import UsersFilters from "@/components/Users/Filter"; import UsersFilters from "@/components/Users/Filter";
export default function Users() { export default function Users() {
const [ searchParams, setSearchParams ] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const isCreate = location.pathname === "/dashboard/users/create"; const isCreate = location.pathname === "/dashboard/users/create";
const isEdit = location.pathname.includes("/edit"); const isEdit = location.pathname.includes("/edit");
const editId = useMemo(() => { const editId = useMemo(() => {
if (isEdit) { if (isEdit) {
return location.pathname.split("/")[3] return location.pathname.split("/")[3];
} }
return null return null;
}, [location, isEdit]) }, [location, isEdit]);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
navigate(`/dashboard/users${searchParams ? `?${searchParams.toString()}` : ""}`); navigate(`/dashboard/users${searchParams ? `?${searchParams.toString()}` : ""}`);
}, [navigate, searchParams]); }, [navigate, searchParams]);
const {data: users, isPending} = useGetUsers(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(() => { const { data: allUsers } = useGetUsers();
return allUsers?.map((user: User) => (user.name))
.filter((season, index, array) => array.indexOf(season) === index) const names = useMemo(() => {
}, [allUsers]) return allUsers
?.map((user: User) => user.name)
const createUserMutation = useCreateUser(); .filter((season, index, array) => array.indexOf(season) === index);
const editUserMutation = useEditUser(); }, [allUsers]);
const handleCreateUser = useCallback(async (user: UserInputs) => { const createUserMutation = useCreateUser();
await createUserMutation.mutateAsync(user); const editUserMutation = useEditUser();
closeModal();
}, [createUserMutation, closeModal]); const handleCreateUser = useCallback(
async (user: UserInputs) => {
const handleEditUser = useCallback(async (user: UserInputs, id?: number) => { await createUserMutation.mutateAsync(user);
if (!id) closeModal();
return; },
await editUserMutation.mutateAsync({ [createUserMutation, closeModal],
id: id, );
user: user
}); const handleEditUser = useCallback(
closeModal(); async (user: UserInputs, id?: number) => {
}, [editUserMutation, closeModal]); if (!id) return;
await editUserMutation.mutateAsync({
const onFilterChange = useCallback((values: string[], filter: string) => { id: id,
setSearchParams(prev => { user: user,
const params = new URLSearchParams(prev); });
params.delete(filter); closeModal();
},
values.forEach(value => { [editUserMutation, closeModal],
params.append(filter, value); );
});
return params; const onFilterChange = useCallback(
}); (values: string[], filter: string) => {
}, [setSearchParams]) setSearchParams((prev) => {
const params = new URLSearchParams(prev);
if (!users || isPending) params.delete(filter);
return (
<Group values.forEach((value) => {
align="center" params.append(filter, value);
justify="center" });
h="80vh" return params;
w="100%" });
> },
<Loader color="pink"/> [setSearchParams],
</Group> );
);
if (!users || isPending)
return ( return (
<Stack> <Group align="center" justify="center" h="80vh" w="100%">
<Group justify="space-between"> <Loader color="pink" />
<Title order={2}>{t("all users", {capfirst: true})}</Title> </Group>
<Tooltip label={t("create user", {capfirst: true})}> );
<ActionIcon
onClick={(e) => { return (
e.stopPropagation(); <Stack>
navigate(`/dashboard/users/create${searchParams ? `?${searchParams.toString()}` : ""}`); <Group justify="space-between">
}} <Title order={2}>{t("all users", { capfirst: true })}</Title>
> <Tooltip label={t("create user", { capfirst: true })}>
<IconPlus/> <ActionIcon
</ActionIcon> onClick={(e) => {
</Tooltip> e.stopPropagation();
<UserModal navigate(
key={`${currentUser?.id}_create`} `/dashboard/users/create${searchParams ? `?${searchParams.toString()}` : ""}`,
opened={isCreate} );
onClose={closeModal} }}
handleSubmit={handleCreateUser} >
/> <IconPlus />
<UserModal </ActionIcon>
key={`${currentUser?.id}_edit`} </Tooltip>
opened={isEdit} <UserModal
onClose={closeModal} key={`${currentUser?.id}_create`}
currentUser={currentUser} opened={isCreate}
handleSubmit={handleEditUser} onClose={closeModal}
/> handleSubmit={handleCreateUser}
</Group> />
<UsersFilters <UserModal
names={names || []} key={`${currentUser?.id}_edit`}
filters={searchParams} opened={isEdit}
onFilterChange={onFilterChange} onClose={closeModal}
/> currentUser={currentUser}
<ScrollArea type="auto"> handleSubmit={handleEditUser}
<Table striped> />
<Table.Thead> </Group>
<Table.Tr> <UsersFilters
<Table.Th>{t("name", {capfirst: true})}</Table.Th> names={names || []}
<Table.Th>{t("email", {capfirst: true})}</Table.Th> filters={searchParams}
<Table.Th>{t("actions", {capfirst: true})}</Table.Th> onFilterChange={onFilterChange}
</Table.Tr> />
</Table.Thead> <ScrollArea type="auto">
<Table.Tbody> <Table striped>
{ <Table.Thead>
users.map((user) => ( <Table.Tr>
<UserRow <Table.Th>{t("name", { capfirst: true })}</Table.Th>
user={user} <Table.Th>{t("email", { capfirst: true })}</Table.Th>
key={user.id} <Table.Th>{t("actions", { capfirst: true })}</Table.Th>
/> </Table.Tr>
)) </Table.Thead>
} <Table.Tbody>
</Table.Tbody> {users.map((user) => (
</Table> <UserRow user={user} key={user.id} />
</ScrollArea> ))}
</Stack> </Table.Tbody>
); </Table>
} </ScrollArea>
</Stack>
);
}

View File

@@ -3,13 +3,13 @@ import { Navbar } from "@/components/Navbar";
import { Footer } from "@/components/Footer"; import { Footer } from "@/components/Footer";
export default function Root() { export default function Root() {
return ( return (
<> <>
<Navbar /> <Navbar />
<main style={{display: "flex", justifyContent: "center"}}> <main style={{ display: "flex", justifyContent: "center" }}>
<Outlet /> <Outlet />
</main> </main>
<Footer /> <Footer />
</> </>
); );
} }

View File

@@ -1,6 +1,4 @@
import { import { createBrowserRouter } from "react-router";
createBrowserRouter,
} from "react-router";
import Root from "@/root"; import Root from "@/root";
import { Home } from "@/pages/Home"; import { Home } from "@/pages/Home";
@@ -14,35 +12,36 @@ import { Contract } from "./pages/Contract";
import { NotFound } from "./pages/NotFound"; import { NotFound } from "./pages/NotFound";
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {
path: "/", path: "/",
Component: Root, Component: Root,
errorElement: <NotFound />, errorElement: <NotFound />,
children: [
{ index: true, Component: Home },
{ path: "/forms", Component: Forms },
{
path: "/dashboard", Component: Dashboard,
children: [ children: [
{ path: "productors", Component: Productors }, { index: true, Component: Home },
{ path: "productors/create", Component: Productors }, { path: "/forms", Component: Forms },
{ path: "productors/:id/edit", Component: Productors }, {
{ path: "products", Component: Products }, path: "/dashboard",
{ path: "products/create", Component: Products }, Component: Dashboard,
{ path: "products/:id/edit", Component: Products }, children: [
// { path: "templates", Component: Templates }, { path: "productors", Component: Productors },
{ path: "users", Component: Users }, { path: "productors/create", Component: Productors },
{ path: "users/create", Component: Users }, { path: "productors/:id/edit", Component: Productors },
{ path: "users/:id/edit", Component: Users }, { path: "products", Component: Products },
{ path: "forms", Component: Forms }, { path: "products/create", Component: Products },
{ path: "forms/:id/edit", Component: Forms }, { path: "products/:id/edit", Component: Products },
{ path: "forms/create", Component: Forms }, // { path: "templates", Component: Templates },
{ path: "shipments", Component: Shipments }, { path: "users", Component: Users },
{ path: "shipments/:id/edit", Component: Shipments }, { path: "users/create", Component: Users },
{ path: "shipments/create", Component: Shipments }, { path: "users/:id/edit", Component: Users },
] { path: "forms", Component: Forms },
}, { path: "forms/:id/edit", Component: Forms },
{ path: "/form/:id", Component: Contract}, { path: "forms/create", Component: Forms },
], { path: "shipments", Component: Shipments },
}, { path: "shipments/:id/edit", Component: Shipments },
{ path: "shipments/create", Component: Shipments },
],
},
{ path: "/form/:id", Component: Contract },
],
},
]); ]);

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 { Config } from "@/config/config";
import type { Form, FormCreate, FormEditPayload } from "@/services/resources/forms"; import type { Form, FormCreate, FormEditPayload } from "@/services/resources/forms";
import type { Shipment, ShipmentCreate, ShipmentEditPayload } from "@/services/resources/shipments"; 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 { User, UserCreate, UserEditPayload } from "@/services/resources/users";
import type { Product, ProductCreate, ProductEditPayload } from "./resources/products"; import type { Product, ProductCreate, ProductEditPayload } from "./resources/products";
import type { ContractCreate } from "./resources/contracts"; import type { ContractCreate } from "./resources/contracts";
@@ -10,559 +20,558 @@ import { notifications } from "@mantine/notifications";
import { t } from "@/config/i18n"; import { t } from "@/config/i18n";
export function useGetShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> { export function useGetShipments(filters?: URLSearchParams): UseQueryResult<Shipment[], Error> {
const queryString = filters?.toString() const queryString = filters?.toString();
return useQuery<Shipment[]>({ return useQuery<Shipment[]>({
queryKey: ['shipments', queryString], queryKey: ["shipments", queryString],
queryFn: () => ( queryFn: () =>
fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`) fetch(`${Config.backend_uri}/shipments${filters ? `?${queryString}` : ""}`).then(
.then((res) => res.json()) (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>({ return useQuery<Shipment>({
queryKey: ['shipment'], queryKey: ["shipment"],
queryFn: () => ( queryFn: () => fetch(`${Config.backend_uri}/shipments/${id}`).then((res) => res.json()),
fetch(`${Config.backend_uri}/shipments/${id}`) enabled: !!id,
.then((res) => res.json())
),
enabled: !!id,
...options, ...options,
}); });
} }
export function useCreateShipment() { export function useCreateShipment() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (newShipment: ShipmentCreate) => { mutationFn: (newShipment: ShipmentCreate) => {
return fetch(`${Config.backend_uri}/shipments`, { return fetch(`${Config.backend_uri}/shipments`, {
method: 'POST', method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(newShipment), body: JSON.stringify(newShipment),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully created shipment", {capfirst: true}), message: t("successfully created shipment", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['shipments'] }) await queryClient.invalidateQueries({ queryKey: ["shipments"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing shipment`, {capfirst: true}), message: error?.message || t(`error editing shipment`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}) });
} }
export function useEditShipment() { export function useEditShipment() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({shipment, id}: ShipmentEditPayload) => { mutationFn: ({ shipment, id }: ShipmentEditPayload) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, { return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: 'PUT', method: "PUT",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(shipment), body: JSON.stringify(shipment),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully edited shipment", {capfirst: true}), message: t("successfully edited shipment", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['shipments'] }) await queryClient.invalidateQueries({ queryKey: ["shipments"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing shipment`, {capfirst: true}), message: error?.message || t(`error editing shipment`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}) });
} }
export function useDeleteShipment() { export function useDeleteShipment() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/shipments/${id}`, { return fetch(`${Config.backend_uri}/shipments/${id}`, {
method: 'DELETE', method: "DELETE",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully deleted shipment", {capfirst: true}), message: t("successfully deleted shipment", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['shipments'] }) await queryClient.invalidateQueries({ queryKey: ["shipments"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting shipment`, {capfirst: true}), message: error?.message || t(`error deleting shipment`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}); });
} }
export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Productor[], Error> { export function useGetProductors(filters?: URLSearchParams): UseQueryResult<Productor[], Error> {
const queryString = filters?.toString() const queryString = filters?.toString();
return useQuery<Productor[]>({ return useQuery<Productor[]>({
queryKey: ['productors', queryString], queryKey: ["productors", queryString],
queryFn: () => ( queryFn: () =>
fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`) fetch(`${Config.backend_uri}/productors${filters ? `?${queryString}` : ""}`).then(
.then((res) => res.json()) (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>({ return useQuery<Productor>({
queryKey: ['productor'], queryKey: ["productor"],
queryFn: () => ( queryFn: () => fetch(`${Config.backend_uri}/productors/${id}`).then((res) => res.json()),
fetch(`${Config.backend_uri}/productors/${id}`) enabled: !!id,
.then((res) => res.json())
),
enabled: !!id,
...options, ...options,
}); });
} }
export function useCreateProductor() { export function useCreateProductor() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (newProductor: ProductorCreate) => { mutationFn: (newProductor: ProductorCreate) => {
return fetch(`${Config.backend_uri}/productors`, { return fetch(`${Config.backend_uri}/productors`, {
method: 'POST', method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(newProductor), body: JSON.stringify(newProductor),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully created productor", {capfirst: true}), message: t("successfully created productor", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['productors'] }) await queryClient.invalidateQueries({ queryKey: ["productors"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing productor`, {capfirst: true}), message: error?.message || t(`error editing productor`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}) });
} }
export function useEditProductor() { export function useEditProductor() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({productor, id}: ProductorEditPayload) => { mutationFn: ({ productor, id }: ProductorEditPayload) => {
return fetch(`${Config.backend_uri}/productors/${id}`, { return fetch(`${Config.backend_uri}/productors/${id}`, {
method: 'PUT', method: "PUT",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(productor), body: JSON.stringify(productor),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully edited productor", {capfirst: true}), message: t("successfully edited productor", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['productors'] }) await queryClient.invalidateQueries({ queryKey: ["productors"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing productor`, {capfirst: true}), message: error?.message || t(`error editing productor`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}) });
} }
export function useDeleteProductor() { export function useDeleteProductor() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/productors/${id}`, { return fetch(`${Config.backend_uri}/productors/${id}`, {
method: 'DELETE', method: "DELETE",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully deleted productor", {capfirst: true}), message: t("successfully deleted productor", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['productors'] }) await queryClient.invalidateQueries({ queryKey: ["productors"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting productor`, {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>({ return useQuery<Form>({
queryKey: ['form'], queryKey: ["form"],
queryFn: () => ( queryFn: () => fetch(`${Config.backend_uri}/forms/${id}`).then((res) => res.json()),
fetch(`${Config.backend_uri}/forms/${id}`) enabled: !!id,
.then((res) => res.json())
),
enabled: !!id,
...options, ...options,
}); });
} }
export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> { export function useGetForms(filters?: URLSearchParams): UseQueryResult<Form[], Error> {
const queryString = filters?.toString() const queryString = filters?.toString();
return useQuery<Form[]>({ return useQuery<Form[]>({
queryKey: ['forms', queryString], queryKey: ["forms", queryString],
queryFn: () => ( queryFn: () =>
fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`) fetch(`${Config.backend_uri}/forms${filters ? `?${queryString}` : ""}`).then((res) =>
.then((res) => res.json()) res.json(),
), ),
}); });
} }
export function useCreateForm() { export function useCreateForm() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (newForm: FormCreate) => { mutationFn: (newForm: FormCreate) => {
return fetch(`${Config.backend_uri}/forms`, { return fetch(`${Config.backend_uri}/forms`, {
method: 'POST', method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(newForm), body: JSON.stringify(newForm),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['forms'] }) await queryClient.invalidateQueries({ queryKey: ["forms"] });
} },
}); });
} }
export function useDeleteForm() { export function useDeleteForm() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/forms/${id}`, { return fetch(`${Config.backend_uri}/forms/${id}`, {
method: 'DELETE', method: "DELETE",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully deleted form", {capfirst: true}), message: t("successfully deleted form", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['forms'] }) await queryClient.invalidateQueries({ queryKey: ["forms"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting form`, {capfirst: true}), message: error?.message || t(`error deleting form`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}); });
} }
export function useEditForm() { export function useEditForm() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({id, form}: FormEditPayload) => { mutationFn: ({ id, form }: FormEditPayload) => {
return fetch(`${Config.backend_uri}/forms/${id}`, { return fetch(`${Config.backend_uri}/forms/${id}`, {
method: 'PUT', method: "PUT",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(form), body: JSON.stringify(form),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully edited form", {capfirst: true}), message: t("successfully edited form", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['forms'] }) await queryClient.invalidateQueries({ queryKey: ["forms"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing form`, {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>({ return useQuery<Product>({
queryKey: ['product'], queryKey: ["product"],
queryFn: () => ( queryFn: () => fetch(`${Config.backend_uri}/products/${id}`).then((res) => res.json()),
fetch(`${Config.backend_uri}/products/${id}`) enabled: !!id,
.then((res) => res.json())
),
enabled: !!id,
...options, ...options,
}); });
} }
export function useGetProducts(filters?: URLSearchParams): UseQueryResult<Product[], Error> { export function useGetProducts(filters?: URLSearchParams): UseQueryResult<Product[], Error> {
const queryString = filters?.toString() const queryString = filters?.toString();
return useQuery<Product[]>({ return useQuery<Product[]>({
queryKey: ['products', queryString], queryKey: ["products", queryString],
queryFn: () => ( queryFn: () =>
fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`) fetch(`${Config.backend_uri}/products${filters ? `?${queryString}` : ""}`).then((res) =>
.then((res) => res.json()) res.json(),
), ),
}); });
} }
export function useCreateProduct() { export function useCreateProduct() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (newProduct: ProductCreate) => { mutationFn: (newProduct: ProductCreate) => {
return fetch(`${Config.backend_uri}/products`, { return fetch(`${Config.backend_uri}/products`, {
method: 'POST', method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(newProduct), body: JSON.stringify(newProduct),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully created product", {capfirst: true}), message: t("successfully created product", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['products'] }) await queryClient.invalidateQueries({ queryKey: ["products"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing product`, {capfirst: true}), message: error?.message || t(`error editing product`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}); });
} }
export function useDeleteProduct() { export function useDeleteProduct() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/products/${id}`, { return fetch(`${Config.backend_uri}/products/${id}`, {
method: 'DELETE', method: "DELETE",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully deleted product", {capfirst: true}), message: t("successfully deleted product", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['products'] }) await queryClient.invalidateQueries({ queryKey: ["products"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting product`, {capfirst: true}), message: error?.message || t(`error deleting product`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}); });
} }
export function useEditProduct() { export function useEditProduct() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({id, product}: ProductEditPayload) => { mutationFn: ({ id, product }: ProductEditPayload) => {
return fetch(`${Config.backend_uri}/products/${id}`, { return fetch(`${Config.backend_uri}/products/${id}`, {
method: 'PUT', method: "PUT",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(product), body: JSON.stringify(product),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully edited product", {capfirst: true}), message: t("successfully edited product", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['products'] }) await queryClient.invalidateQueries({ queryKey: ["products"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing product`, {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>({ return useQuery<User>({
queryKey: ['user'], queryKey: ["user"],
queryFn: () => ( queryFn: () => fetch(`${Config.backend_uri}/users/${id}`).then((res) => res.json()),
fetch(`${Config.backend_uri}/users/${id}`) enabled: !!id,
.then((res) => res.json())
),
enabled: !!id,
...options, ...options,
}); });
} }
export function useGetUsers(filters?: URLSearchParams): UseQueryResult<User[], Error> { export function useGetUsers(filters?: URLSearchParams): UseQueryResult<User[], Error> {
const queryString = filters?.toString() const queryString = filters?.toString();
return useQuery<User[]>({ return useQuery<User[]>({
queryKey: ['users', queryString], queryKey: ["users", queryString],
queryFn: () => ( queryFn: () =>
fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`) fetch(`${Config.backend_uri}/users${filters ? `?${queryString}` : ""}`).then((res) =>
.then((res) => res.json()) res.json(),
), ),
}); });
} }
export function useCreateUser() { export function useCreateUser() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (newUser: UserCreate) => { mutationFn: (newUser: UserCreate) => {
return fetch(`${Config.backend_uri}/users`, { return fetch(`${Config.backend_uri}/users`, {
method: 'POST', method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(newUser), body: JSON.stringify(newUser),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully created user", {capfirst: true}), message: t("successfully created user", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['users'] }) await queryClient.invalidateQueries({ queryKey: ["users"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing user`, {capfirst: true}), message: error?.message || t(`error editing user`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}); });
} }
export function useDeleteUser() { export function useDeleteUser() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (id: number) => { mutationFn: (id: number) => {
return fetch(`${Config.backend_uri}/users/${id}`, { return fetch(`${Config.backend_uri}/users/${id}`, {
method: 'DELETE', method: "DELETE",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully deleted user", {capfirst: true}), message: t("successfully deleted user", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['users'] }) await queryClient.invalidateQueries({ queryKey: ["users"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error deleting user`, {capfirst: true}), message: error?.message || t(`error deleting user`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}); });
} }
export function useEditUser() { export function useEditUser() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({id, user}: UserEditPayload) => { mutationFn: ({ id, user }: UserEditPayload) => {
return fetch(`${Config.backend_uri}/users/${id}`, { return fetch(`${Config.backend_uri}/users/${id}`, {
method: 'PUT', method: "PUT",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(user), body: JSON.stringify(user),
}).then((res) => res.json()); }).then((res) => res.json());
}, },
onSuccess: async () => { onSuccess: async () => {
notifications.show({ notifications.show({
title: t("success", {capfirst: true}), title: t("success", { capfirst: true }),
message: t("successfully edited user", {capfirst: true}), message: t("successfully edited user", { capfirst: true }),
}); });
await queryClient.invalidateQueries({ queryKey: ['users'] }) await queryClient.invalidateQueries({ queryKey: ["users"] });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ notifications.show({
title: t("error", {capfirst: true}), title: t("error", { capfirst: true }),
message: error?.message || t(`error editing user`, {capfirst: true}), message: error?.message || t(`error editing user`, { capfirst: true }),
color: "red" color: "red",
}); });
} },
}); });
} }
export function useCreateContract() { export function useCreateContract() {
const queryClient = useQueryClient() const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (newContract: ContractCreate) => { mutationFn: (newContract: ContractCreate) => {
return fetch(`${Config.backend_uri}/contracts`, { return fetch(`${Config.backend_uri}/contracts`, {
method: 'POST', method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify(newContract), body: JSON.stringify(newContract),
}).then(async (res) => await res.blob()); }).then(async (res) => await res.blob());
@@ -575,6 +584,6 @@ export function useCreateContract() {
link.click(); link.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
await queryClient.invalidateQueries({ queryKey: ["contracts"] }); await queryClient.invalidateQueries({ queryKey: ["contracts"] });
} },
}); });
} }

View File

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

View File

@@ -1,50 +1,50 @@
import type { Productor } from "@/services/resources/productors"; import type { Productor } from "@/services/resources/productors";
import type { Shipment } from "@/services/resources/shipments"; import type { Shipment } from "@/services/resources/shipments";
import type { User } from "@/services/resources/users"; import type { User } from "@/services/resources/users";
export type Form = { export type Form = {
id: number; id: number;
name: string; name: string;
season: string; season: string;
start: string; start: string;
end: string; end: string;
productor: Productor; productor: Productor;
referer: User; referer: User;
shipments: Shipment[]; shipments: Shipment[];
minimum_shipment_value: number | null; minimum_shipment_value: number | null;
} };
export type FormCreate = { export type FormCreate = {
name: string; name: string;
season: string; season: string;
start: string; start: string;
end: string; end: string;
productor_id: number; productor_id: number;
referer_id: number; referer_id: number;
minimum_shipment_value: number | null; minimum_shipment_value: number | null;
} };
export type FormEdit = { export type FormEdit = {
name?: string | null; name?: string | null;
season?: string | null; season?: string | null;
start?: string | null; start?: string | null;
end?: string | null; end?: string | null;
productor_id?: number | null; productor_id?: number | null;
referer_id?: number | null; referer_id?: number | null;
minimum_shipment_value: number | null; minimum_shipment_value: number | null;
} };
export type FormEditPayload = { export type FormEditPayload = {
id: number; id: number;
form: FormEdit; form: FormEdit;
} };
export type FormInputs = { export type FormInputs = {
name: string; name: string;
season: string; season: string;
start: string | null; start: string | null;
end: string | null; end: string | null;
productor_id: string; productor_id: string;
referer_id: string; referer_id: string;
minimum_shipment_value: number | string | null; minimum_shipment_value: number | string | null;
} };

View File

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

View File

@@ -1,100 +1,106 @@
import type { Productor } from "@/services/resources/productors"; import type { Productor } from "@/services/resources/productors";
import type { Shipment } from "@/services/resources/shipments"; import type { Shipment } from "@/services/resources/shipments";
type ProductTypeKey = "1" | "2"; type ProductTypeKey = "1" | "2";
type ProductUnitKey = "1" | "2" | "3"; type ProductUnitKey = "1" | "2" | "3";
export const ProductType = { export const ProductType = {
"1": "planned", "1": "planned",
"2": "recurrent", "2": "recurrent",
}; };
export const ProductUnit = { export const ProductUnit = {
"1": "grams", "1": "grams",
"2": "kilo", "2": "kilo",
"3": "piece", "3": "piece",
}; };
export const ProductQuantityUnit = { export const ProductQuantityUnit = {
"ml": "mililiter", ml: "mililiter",
"L": "liter", L: "liter",
"g": "grams", g: "grams",
"kg": "kilo" kg: "kilo",
} };
export type Product = { export type Product = {
id: number; id: number;
productor: Productor; productor: Productor;
name: string; name: string;
unit: ProductUnitKey; unit: ProductUnitKey;
price: number | null; price: number | null;
price_kg: number | null; price_kg: number | null;
quantity: number | null; quantity: number | null;
quantity_unit: string | null; quantity_unit: string | null;
type: ProductTypeKey; type: ProductTypeKey;
shipments: Shipment[]; shipments: Shipment[];
} };
export type ProductCreate = { export type ProductCreate = {
productor_id: number; productor_id: number;
name: string; name: string;
unit: string; unit: string;
price: number | null; price: number | null;
price_kg: number | null; price_kg: number | null;
quantity: number | null; quantity: number | null;
quantity_unit: string | null; quantity_unit: string | null;
type: string; type: string;
} };
export type ProductEdit = { export type ProductEdit = {
productor_id: number | null; productor_id: number | null;
name: string | null; name: string | null;
unit: string | null; unit: string | null;
price: number | null; price: number | null;
price_kg: number | null; price_kg: number | null;
quantity: number | null; quantity: number | null;
quantity_unit: string | null; quantity_unit: string | null;
type: string | null; type: string | null;
} };
export type ProductInputs = { export type ProductInputs = {
productor_id: string | null; productor_id: string | null;
name: string; name: string;
unit: string | null; unit: string | null;
price: number | string | null; price: number | string | null;
price_kg: number | string | null; price_kg: number | string | null;
quantity: number | string | null; quantity: number | string | null;
quantity_unit: string | null; quantity_unit: string | null;
type: string | null; type: string | null;
} };
export type ProductEditPayload = { export type ProductEditPayload = {
product: ProductEdit; product: ProductEdit;
id: number; id: number;
} };
export function productToProductInputs(product: Product): ProductInputs { export function productToProductInputs(product: Product): ProductInputs {
return { return {
productor_id: String(product.productor.id), productor_id: String(product.productor.id),
name: product.name, name: product.name,
unit: product.unit, unit: product.unit,
price: product.price, price: product.price,
price_kg: product.price_kg, price_kg: product.price_kg,
quantity: product.quantity, quantity: product.quantity,
quantity_unit: product.quantity_unit, quantity_unit: product.quantity_unit,
type: product.type, type: product.type,
}; };
} }
export function productCreateFromProductInputs(productInput: ProductInputs): ProductCreate { export function productCreateFromProductInputs(productInput: ProductInputs): ProductCreate {
return { return {
productor_id: Number(productInput.productor_id)!, productor_id: Number(productInput.productor_id)!,
name: productInput.name, name: productInput.name,
unit: productInput.unit!, unit: productInput.unit!,
price: productInput.price === "" || !productInput.price ? null : Number(productInput.price), price: productInput.price === "" || !productInput.price ? null : Number(productInput.price),
price_kg: productInput.price_kg === "" || !productInput.price_kg ? null : Number(productInput.price_kg), price_kg:
quantity: productInput.quantity === "" || !productInput.quantity ? null : Number(productInput.quantity), productInput.price_kg === "" || !productInput.price_kg
quantity_unit: productInput.quantity_unit, ? null
type: productInput.type!, : Number(productInput.price_kg),
} quantity:
} productInput.quantity === "" || !productInput.quantity
? null
: Number(productInput.quantity),
quantity_unit: productInput.quantity_unit,
type: productInput.type!,
};
}

View File

@@ -1,54 +1,54 @@
import type { Form } from "./forms"; import type { Form } from "./forms";
import type { Product } from "./products"; import type { Product } from "./products";
export type Shipment = { export type Shipment = {
name: string; name: string;
date: string; date: string;
id: number; id: number;
form: Form; form: Form;
form_id: number; form_id: number;
products: Product[]; products: Product[];
} };
export type ShipmentCreate = { export type ShipmentCreate = {
name: string; name: string;
date: string; date: string;
form_id: number; form_id: number;
product_ids: number[]; product_ids: number[];
} };
export type ShipmentEdit = { export type ShipmentEdit = {
name: string | null; name: string | null;
date: string | null; date: string | null;
form_id: number | null; form_id: number | null;
product_ids: number[]; product_ids: number[];
} };
export type ShipmentEditPayload = { export type ShipmentEditPayload = {
id: number; id: number;
shipment: ShipmentEdit; shipment: ShipmentEdit;
} };
export type ShipmentInputs = { export type ShipmentInputs = {
name: string | null; name: string | null;
date: string | null; date: string | null;
form_id: string | null; form_id: string | null;
product_ids: string[]; product_ids: string[];
} };
export function shipmentToShipmentInputs(shipment: Shipment): ShipmentInputs { export function shipmentToShipmentInputs(shipment: Shipment): ShipmentInputs {
return { return {
...shipment, ...shipment,
form_id: String(shipment.form_id), form_id: String(shipment.form_id),
product_ids: shipment.products.map((el) => (String(el.id))) product_ids: shipment.products.map((el) => String(el.id)),
}; };
} }
export function shipmentCreateFromShipmentInputs(shipmentInput: ShipmentInputs): ShipmentCreate { export function shipmentCreateFromShipmentInputs(shipmentInput: ShipmentInputs): ShipmentCreate {
return { return {
name: shipmentInput.name!, name: shipmentInput.name!,
date: shipmentInput.date!, date: shipmentInput.date!,
form_id: Number(shipmentInput.form_id), 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

@@ -1,28 +1,28 @@
import type { Product } from "@/services/resources/products"; import type { Product } from "@/services/resources/products";
export type User = { export type User = {
id: number; id: number;
name: string; name: string;
email: string; email: string;
products: Product[]; products: Product[];
} };
export type UserInputs = { export type UserInputs = {
email: string; email: string;
name: string; name: string;
} };
export type UserCreate ={ export type UserCreate = {
email: string | null; email: string | null;
name: string | null; name: string | null;
} };
export type UserEdit ={ export type UserEdit = {
email: string | null; email: string | null;
name: string | null; name: string | null;
} };
export type UserEditPayload = { export type UserEditPayload = {
user: UserEdit; user: UserEdit;
id: number; id: number;
} };

View File

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

View File

@@ -1,32 +1,32 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"types": ["vite/client"], "types": ["vite/client"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
} }
}, },
"include": ["src"] "include": ["src"]
} }

View File

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

View File

@@ -1,26 +1,26 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023", "target": "ES2023",
"lib": ["ES2023"], "lib": ["ES2023"],
"module": "ESNext", "module": "ESNext",
"types": ["node"], "types": ["node"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

View File

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