diff --git a/.gitignore b/.gitignore index b3f919b33..9f22ad7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ .AppleDouble .LSOverride -# - Minitatures +# - Miniatures ._* # - Fichiers qui peuvent apparaître à la racine de volumes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 738fb1787..88e6f5efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,28 @@ Ce projet adhère au principe du [Semantic Versioning](https://semver.org/spec/v (il sera toutefois supprimé dans une future version, pensez à mettre à jour vos `settings.json` si vous ne disposez pas d'une offre SaaS) - Ajoute une commande permettant d'envoyer un e-mail de test via la console (Premium). -- L'import de bénéficiaires en masse est maintenant possible depuis un fichier CSV (Premium #305). -- Corrige l'affichage des disponibilités des techniciens à l'étape 3 de la modification d'événement (Premium #361). +- L'import de bénéficiaires en masse est maintenant possible depuis un fichier CSV (Premium). +- Corrige l'affichage des disponibilités des techniciens à l'étape 3 de la modification d'événement. - Ajoute un endpoint `/healthcheck` (désactivé par défaut) pour vérifier l'état de l'instance, - et la date de dernière modification de son matériel, événements ou réservations (Premium #357). + et la date de dernière modification de son matériel, événements ou réservations. +- Corrige le champ de recherche des demandes de réservations. +- Permet la modification du matériel des réservations jusqu'au dernier jour de sortie (Premium). +- Prise en charge des inventaires de départ des événements et réservations. +- Corrige un souci lors de la sauvegarde d'une unité de matériel avec une référence déjà + existante pour le même matériel (l'erreur de sauvegarde faisait penser à un bug de l'application). +- Il est maintenant possible de chercher dans les événements par lieu. +- Prise en charge des retour à la ligne dans l'affichage de description des matériels. +- Corrige l'affichage et le tri des quantités cassées dans le matériel. +- Ajoute une page permettant de consulter les informations d'un bénéficiaire, son historique de commandes, + ainsi que la liste des devis et factures qui lui ont été adressés. +- Corrige la duplication d'événement lorsque des unités de l'événement d'origine sont + déjà utilisées au même moment que dans le nouvel événement (Premium). +- Corrige la duplication d'événement lorsque des techniciens de l'événement d'origine sont + déjà mobilisés au même moment que dans le nouvel événement (voir #346). +- Améliore les sélecteurs de dates, notamment en permettant de choisir des périodes pré-définies quand + c'est utile (par exemple dans les filtres de période matériels et techniciens). +- Corrige un problème de performance lors de la récupération des réservations et événements liés + aux matériels et bénéficiaires. La récupération se fait maintenant de manière séquentielle (voir #387). ## 0.22.2 (2023-08-11) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 2f12d3e6c..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,303 +0,0 @@ -# Guide de contribution - -## Pré-requis - -Pour pouvoir commencer à développer sur ce projet, votre IDE doit être configuré pour -prendre en compte les configurations `.editorconfig`, ESLint et PHP code sniffer, -ce qui nous permet de nous assurer de l'homogénéité du formatage des fichiers soumis dans le projet. - -En plus des ["outils" globaux requis](https://robertmanager.org/wiki/install#install-before) pour une installation normale de Robert2 (PHP, MySQL, etc), -vous devrez aussi installer la dernière version de __[Node](https://nodejs.org/fr/)__ pour pouvoir lancer les commandes de build -(si vous touchez à la partie front). - -Pour pouvoir générer un rapport de couverture en HTML (pour la partie serveur), il faut que __xDebug__ soit installé sur votre système. -Vous pouvez l'installer via la commande suivante (sur les systèmes Debian-like) : `sudo apt install php-xdebug`. - -À noter que dans ce guide de contribution, nous utiliserons __[Yarn v1](https://classic.yarnpkg.com/fr/)__ pour les commandes côté client. -Nous vous invitons donc à l'installer et à l'utiliser lorsque vous intervenez sur Robert2. - -### Pour les développeurs sous MacOs / Windows - -Cette application est avant tout pensée pour être développée dans un environnement Linux-like. - -Par exemple, dans l'installation de développement, des liens symboliques sont utilisés à certains endroits -(`/server/src/VERSION` et `/server/src/public/webclient`) et ceux-ci devront être re-créés manuellement avec leur équivalent -sous windows qui ne supporte pas les liens symboliques tel qu'ils apparaissent dans le repository. - -De la même façon, sous MacOs, certains des utilitaires globaux (tel que `sed`, `grep`, etc.) diffèrent des utilitaires GNU utilisés sous Linux. -Pour ceux-ci, nous vous conseillons d'installer les paquets [Homebrew](https://brew.sh/index_fr) liés (`coreutils`, `gnu-sed`, etc.) et de mettre -ces exécutables par défaut via votre $PATH. -(ceci sera au moins à faire pour `grep` et `sed` sans quoi vous ne pourrez pas exécuter le script de releasing) - -## Installation - -Pour ce qui est de l'installation de Robert2 en lui-même, veuillez suivre [la procédure d'installation avancée](https://robertmanager.org/wiki/install) -telle que documentée sur le site de Robert, il n'y a pas de changement par rapport à celle-ci. - -Pour ce qui est de la partie client, veuillez vous rendre à la racine du dossier `client/` et exécutez `yarn install`. - -## Langue du projet - -Nous avons fait le choix de communiquer exclusivement en __français__ partout dans les outils liés à Robert. -Les fichiers markdown, commentaires, entrées de CHANGELOG, outils CLI, messages de commit et tickets -doivent donc être rédigés en français uniquement. - -Le code, lui, à l'exception des commentaires, doit être exclusivement en anglais, pas de variable ou de nom de migration en français. - -Attention ⚠️, cela ne veut pas dire que Robert n'est pas traduit : Robert2 est aussi disponible en anglais et doit continuer à l'être. -Merci donc de bien vouloir prendre en compte le fait que chaque texte affiché dans l'interface de Robert doit pouvoir être traduit et si possible, -veuillez spécifier les traductions anglaises de vos ajouts en français dans vos pull requests. - -## Branches Git - -Pour nommer ses branches, le projet utilise le modèle appelé « Git Flow ». -Voir [cette page](https://git-flow.readthedocs.io/fr/latest/presentation.html) pour plus de détails sur ce workflow, -mais voici un tuto rapide : - -Les deux branches principales qui **existent en permanence** sont : -- `master` : la branche sur laquelle se trouve l'application telle qu'elle a été releasée en dernier. - On ne peut y merger que des branches `release/x.x.x` ou `hotfix/x.x.x`. Aucun commit ne doit y être ajouté directement. - Cette branche est considérée comme la "branche stable releasée". -- `develop` : la branche sur laquelle on merge toutes les branches de nouvelles fonctionnalités (nommées `feature/...`). - On peut aussi y faire quelques commits directs, uniquement quand il s'agit de petites corrections. - Cette branche est considérée comme la "branche stable non-releasée". - -Quand vous voulez modifier le code, commencez par créer une branche `feature/nom-de-la-fonctionnalité`, qui est basée sur `develop`. -Ensuite, utilisez cette branche pour créer une pull-request dont la branche de destination est `develop`. -Avant d'être fusionnée, le fonctionnement de l'application sur cette branche doit impérativement être stable et dépourvu de bug. -Une fois la PR fusionnée, la branche doit être supprimée. - -## Version et Changelog - -Robert2 utilise la nomenclature de version [Semantic Versioning (semver)](https://semver.org/) pour ses numéros de version. La version actuelle qui -correspond à celle se trouvant dans la branche `master` est définie dans le fichier `/VERSION`. - -Un fichier de changelog est présent à la racine du projet, montrant l'évolution des fonctionnalités au fil du temps et des versions. -__Il est (et doit être) impérativement maintenu à jour.__ - -## Releasing - -Pour créer une release de Robert2, veuillez suivre les étapes suivantes : - -1. Exécutez `./bin/release -v [NuméroDeVersion]` en étant à la racine du projet. - (Note : si vous ne spécifiez pas de version, la version actuellement dans le fichier `/VERSION` sera utilisée. - Vous pouvez aussi spécifier le terme `testing` pour la version, afin de créer une release temporaire qui ne - met pas à jour le Changelog ni le fichier de version). -2. Terminé ! Vous pouvez récupérer le fichier ZIP qui a été créé dans le dossier `/dist`. - -## Build de la partie client - -Si vous intervenez sur la partie client, vous aurez besoin de compiler celle-ci pour que votre instance locale -reflète directement les changements que vous apportez au code. - -Pour cela, vous avez à disposition deux commandes (à exécuter depuis la racine du dossier `/client`) : - -#### `yarn start` - -C'est cette méthode qu'il faudra utiliser pendant la plupart de vos développements front-end. - -Cette commande vous permet de lancer un serveur de développement front-end, avec prise en charge du Hot Reloading, -qui servira les sources JS, CSS et les assets, à l'adresse `http://localhost:8081/`. - -Pour travailler, créez un fichier `.env` dans le dossier `server/` qui contient la variable `APP_ENV=development`, -puis ouvrez l'application sur son serveur back-end (par ex. `http://loxya.test`). - -#### `yarn build` - -Cette commande va créer un build de production de la partie client en compilant et compressant les sources. -_(Pensez à exécuter cette commande et à commiter le résultat dans votre PR lorsque vous modifiez la partie client)_ - -## URL de l'API en développement - -En développement, l'hôte par défaut utilisé par la partie client pour communiquer avec l'API est `http://loxya.test/`. - -Si vous souhaitez modifier ceci, vous pouvez créer un fichier `.env.development.local` à la racine du dossier -client et surcharger la variable d'environnement `VUE_APP_API_URL` avec votre propre URL d'API (par -exemple `http://localhost/loxya`). - -## Migration de la base de données - -Nous utilisons [Phinx](https://phinx.org/) pour les mises à jour de la structure de la base de données. -C'est pourquoi vous ne devez pas modifier le schéma "manuellement". À la place, créez un fichier de migration -via la commande suivante : - -```bash -composer create-migration [NameOfYourMigration] -``` - -Merci d'être précis dans le nommage de vos migrations, par exemple : `composer create-migration AddEmailToTechnicians`. - -Ensuite, vous pourrez utiliser les commandes suivantes : - -```bash -composer migration-status # Affiche le statut de migration de votre base de données. -composer migrate # Migration de votre base de données en prenant en compte tous les fichiers de migration non exécutés. -composer rollback # Annule la dernière migration exécutée sur votre base de données (peut être lancée plusieurs fois). -``` - -## Tests unitaires - -Nous utilisons __Jest__ pour les tests unitaires côté front. Il n'y a pas de pre-requis, il suffit -de les lancer comme ceci : - -```bash -# - Se placer dans le dossier client/ -cd client - -# - Lancer tous les tests -yarn test - -# - Ou bien, en mode watch -yarn test --watch -``` - -Pour les tests unitaires côté back, nous utilisons __PHPUnit__. Pour ceux-ci, vous aurez besoin d'une -base de données dédiée aux tests. - -Pour pouvoir être exécutées, il faudra définir certaines variables d'environnement pour configurer -l'accès à votre base de données de test. - -Ceci peut-être effectué en créant un fichier `.env.test` à la racine de votre dossier `/server` : - -__*Exemple de fichier `.env.test`*__ -```env -DB_HOST='localhost' # - L'hôte de votre base de données. -DB_TEST='Loxya-Test' # - Le nom de votre base de données de test. -DB_USER='root' # - L'utilisateur de votre base de données. -DB_PASS='' # - Le mot de passe d'accès à votre base de données. -DB_PORT=3306 # - Le port de connexion à votre base de données. -``` - -⚠️ Sans ces variables d'environnement, les tests ne pourront pas s'exécuter correctement -vu que ceux-ci n'utilisent pas de fichier `settings.json` comme c'est le cas pour l'application -lorsqu'elle est utilisée "normalement". - -Ensuite, vous pouvez exécuter les tests via : - -```bash -# - Se placer dans le dossier server/ -cd server - -# - Lancer tous les tests -composer test - -# - On peut aussi ne lancer qu'un seul fichier de test en particulier, par ex. : -composer test -- --filter EventTest # - Lance les tests du fichier tests/models/EventTest.php -``` - -### Qu'est-ce que l'on teste ? - -Côté back, des tests unitaires doivent être mis en place au moins pour tous les modèles, les routes d'API -ainsi que pour les fonctions et classes utilitaires. - -Côté front, tous les utilitaires doivent être testés. - -N'hésitez pas, bien sûr, à tester aussi des parties du code qui ne sont pas spécifiées ci-dessus, -plus il y a de test, mieux c'est ! - -## Linting - -Le projet suit des règles strictes de linting (avec PHPcs pour le back et ESLint pour le Front). -Un fichier `.editorconfig` existe à la racine du projet pour permettre aux IDE d'automatiser la -présentation de base du code (voir [editorconfig.org](https://editorconfig.org/), ainsi que -[l'extension VSCode](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) dédiée). - -Vous avez la possibilité de vérifier que votre code respecte bien ces conventions via : - -```bash -# - Pour la partie client -yarn lint - -# - Pour la partie serveur -composer lint -``` - -## Structure de l'application - -``` -. -├── bin # - Executables globaux (`./bin/release`, etc.) -│ -├── client -│   ├── dist # - Contient les sources compilées de la partie client. -│   ├── node_modules # - Dépendances de la partie client. -│   ├── src -│   │   ├── components # - Components Vue réutilisables partout (dans tous les thèmes). -│   │   ├── globals # - Fichiers de configuration de la partie client (constantes, configuration globale, etc.). -│   │   ├── hooks # - Hooks utilisables dans les composants créés avec le composition-API de Vue. -│   │   ├── locale # - Fichiers de traduction de la partie client dans les différentes langues supportées. -│   │   ├── stores # - Contient les différents stores (Vuex) globaux de l'application. -│   │   ├── themes # - Contient les différents thèmes de Robert2. -│   │   │   ├── default # - Thème par défaut (partie admin) -| │   │   | ├── components # - Dossier des composants partagés dans ce thème. -│   │   │   │  ├── globals # - Fichiers de configuration de ce thème. -│   │   │   │  ├── layout # - Composants du layout du thème. -│   │   │   │  ├── locale # - Dossier des traductions spécifiques au thème. -│   │   │   │  ├── modals # - Chaque sous-dossier représente une fenêtre modale de ce thème. -| │   │   | ├── pages # - Chaque sous-dossier représente une page de ce thème. -│   │   │   │  ├── stores # - Contient les différents Stores globaux (Vuex) du thème. -| │   │   | └── style # - Contient le style global du thème (reset, fonts, styles de base, variables globales, etc.). -| │   │   | └── index.js # - Point d'entrée du thème. -| │   │   | └── index.scss # - Point d'entrée du style du thème. -│   │   │   ├── external # - Thème de la partie "externe" (réservations en ligne) -| │   │   | ├── components # - Dossier des composants partagés dans ce thème. -│   │   │   │  ├── globals # - Fichiers de configuration de ce thème. -│   │   │   │  ├── layout # - Composants du layout du thème. -│   │   │   │  ├── locale # - Dossier des traductions spécifiques au thème. -│   │   │   │  ├── modals # - Chaque sous-dossier représente une fenêtre modale de ce thème. -| │   │   | ├── pages # - Chaque sous-dossier représente une page de ce thème. -│   │   │   │  ├── stores # - Contient les différents Stores globaux (Vuex) du thème. -| │   │   | └── style # - Contient le style global du thème (reset, fonts, styles de base, variables globales, etc.). -| │   │   | └── index.js # - Point d'entrée du thème. -| │   │   | └── index.scss # - Point d'entrée du style du thème. -│   │   └── utils # - Fonctions JS utilitaires -│   └── tests # - Contient les tests unitaires (Jest) de la partie client. -│ -└── server - ├── bin # - Fichiers exécutables (voir dossier `src/App/Console`) - ├── data # - Fichiers associés aux données (matériel, etc.) - ├── src - │   ├── App # - Modèles, controller, configurations et autres fichiers du cœur de l'application. - │   │   ├── Config # - Configuration, ACLs et constantes et fonctions globales. - │   │   ├── Console # - Commandes exécutables en CLI (migrations, notifications...). - │   │   ├── Contracts # - Interfaces PHP. - │   │   ├── Controllers # - Contrôleurs de l'application (contenant principalement les endpoints d'API) - │   │   ├── Errors # - Gestion des erreurs et classes d'exceptions customs. - │   │   ├── Http # - Classes spécifiques au contexte HTTP - │   │   ├── Middlewares # - Middlewares Slim (ACL, JWT Auth, pagination, etc.). - │   │   ├── Models # - Modèles (Eloquent) de l'application. - │   │   ├── Observers # - Classes pour déclencher des side-effects suite à un changement dans les models. - │   │   ├── Services # - Contient les services, comme le système d'authentification. - │   │   ├── Support # - Contient les classes utilitaires. - │   │   └── App.php # - Classe principale l'application Slim. - │   │   └── Kernel.php # - Point de départ de l'application. - │   ├── install # - Classes et utilitaires liés à l'assistant d'installation de Robert2. - │   ├── locales # - Fichiers de traduction de la partie serveur dans les différentes langues supportées. -    │   ├── migrations # - Fichiers de migration de la base de données (générés via `composer create-migration [MigrationName]`) - │   ├── public - │   │   ├── icons/ # - Images d'icônes et favicon de l'application. - │   │   ├── install/ # - Dossiers contenant des fichiers d'asset utilisés spécifiquement dans les vues du wizard d'installation. - │   │   ├── webclient/ # - Lien symbolique vers les sources compilées de la partie `/client` de Robert2. - │   │   └── index.php # - Point d'entrée de l'application (tous les `.htaccess` redirigent vers ce fichier). - │   ├── var - │   │   ├── cache # - Fichiers de cache (contenu à supprimer en cas de modification du code qui semble sans effet) - │   │   ├── logs # - Fichiers de log de l'application. - │   │   └── tmp # - Fichiers temporaires. - │   └── views # - Dossier contenant les vues Twig de l'application. - │   │   ├── blocks # - Les blocks communs, comme le loading, etc. - │   │   ├── install # - Toutes les pages de l'assistant d'installation - │   │   ├── pdf # - Les vues des sorties PDF (factures, fiches d'événement, etc.) - │   │   ├── webclient.twig # - Point d'entrée de l'application Robert2 (front-end) - │   │   └── install.twig # - Point d'entrée de l'assistant d'installation - ├── tests - │   ├── commands # - Tests unitaires (PHPUnit) des commandes (voir ). - │   ├── endpoints # - Tests unitaires (PHPUnit) des controllers. - │   ├── Fixtures - │   │   ├── files # - Fichiers associés aux données (voir server/data) à utiliser pour les fixtures. - │   │   ├── seed # - Données utilisées pour les tables de la DB de test, au format JSON. - │   │   └── tmp # - Dossier utilisé pour stocker la structure SQL (créée à la volée) de la DB de test, pour reset. - │   ├── libs # - Tests unitaires (PHPUnit) des libs. - │   ├── models # - Tests unitaires (PHPUnit) des modèles. - │   └── other # - Tests unitaires (PHPUnit) des fonctions utilitaires et autres classes. - ├── vendors # - Dépendances (composer) de la partie serveur. -``` diff --git a/bin/release b/bin/release index ea1850a18..0013f7046 100755 --- a/bin/release +++ b/bin/release @@ -101,8 +101,8 @@ chmod 777 src/install echo -e "\e[1m\e[34m-> Installation des dépendances back-end...\e[0m" query=( 'del(."require-dev")' - 'del(.autoload."psr-4"."Robert2\\Tests\\")' - 'del(.autoload."psr-4"."Robert2\\Fixtures\\")' + 'del(.autoload."psr-4"."Loxya\\Tests\\")' + 'del(.autoload."psr-4"."Loxya\\Fixtures\\")' 'del(.scripts."create-migration")' 'del(.scripts."lint")' 'del(.scripts."lint:fix")' diff --git a/client/package.json b/client/package.json index 510aa7286..c1b9eb0fc 100644 --- a/client/package.json +++ b/client/package.json @@ -24,6 +24,7 @@ "moment": "2.29.4", "p-defer": "3.0.0", "p-queue": "6.6.2", + "papaparse": "5.4.1", "portal-vue": "2.1.7", "status-code-enum": "~1.0.0", "style-object-to-css-string": "1.0.1", @@ -42,7 +43,8 @@ "vue2-datepicker": "3.11.1", "vuex": "3.6.2", "vuex-i18n": "1.13.1", - "warning": "4.0.3" + "warning": "4.0.3", + "zod": "3.22.4" }, "devDependencies": { "@babel/core": "7.21.4", @@ -55,6 +57,7 @@ "@types/jest": "29", "@types/js-cookie": "3.0", "@types/lodash": "4", + "@types/papaparse": "5.3.7", "@types/tinycolor2": "1", "@vue/babel-preset-app": "4.5.15", "@vue/cli-plugin-babel": "4.5.13", @@ -75,6 +78,7 @@ "sass": "1.62.0", "sass-loader": "10.2.0", "stylelint": "14.1.0", + "type-fest": "4.4.0", "typescript": "5.0.4", "vue-cli-plugin-svg": "0.1.3", "vue-cli-plugin-yaml": "1.0.2", diff --git a/client/src/globals/types/composition.d.ts b/client/src/globals/types/composition.d.ts index ea1c7b6ce..078ca9ce8 100644 --- a/client/src/globals/types/composition.d.ts +++ b/client/src/globals/types/composition.d.ts @@ -1,9 +1,4 @@ import type { VNode } from 'vue'; -import type { - ComponentPropsOptions, - EmitsOptions, - SetupContext, -} from '@vue/composition-api'; module '@vue/composition-api' { export type Render = () => VNode | null; @@ -11,16 +6,4 @@ module '@vue/composition-api' { export type ImplicitProps = { key?: string | number, }; - - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - type ComponentReturnType = Record | Render | void; - - export interface Component< - Props = Record, - ReturnType extends ComponentReturnType = Render, - > { - (props: Props & ImplicitProps, ctx: SetupContext): ReturnType; - props?: ComponentPropsOptions>; - emits?: EmitsOptions; - } } diff --git a/client/src/globals/types/vendors/vue-js-modal.d.ts b/client/src/globals/types/vendors/vue-js-modal.d.ts index c0185134a..9b7c0baaa 100644 --- a/client/src/globals/types/vendors/vue-js-modal.d.ts +++ b/client/src/globals/types/vendors/vue-js-modal.d.ts @@ -1,3 +1,5 @@ +import 'vue-js-modal'; + declare module 'vue-js-modal' { export type OnCloseEvent> = { /** Le nom de la modale (créé dynamiquement par vue-js-modal). */ diff --git a/client/src/globals/types/vendors/vue-tables-2.d.ts b/client/src/globals/types/vendors/vue-tables-2.d.ts index 4d0d67fe9..363bca6a2 100644 --- a/client/src/globals/types/vendors/vue-tables-2.d.ts +++ b/client/src/globals/types/vendors/vue-tables-2.d.ts @@ -1,12 +1,31 @@ declare module 'vue-tables-2-premium' { - import type { VNode } from 'vue'; + import type { CreateElement, VNode, VNodeClass } from 'vue'; import type { PaginationParams } from '@/stores/api/@types'; // // - Common types // - type BaseTableOptions = { + type ColumnVisibility = + | `${'min' | 'not' | 'max'}_mobile` + | `${'min' | 'not' | 'max'}_mobileP` + | `${'min' | 'not' | 'max'}_mobileL` + | `${'min' | 'not' | 'max'}_tablet` + | `${'min' | 'not' | 'max'}_tabletP` + | `${'min' | 'not' | 'max'}_tabletL` + | `${'min' | 'not' | 'max'}_desktop` + | `${'min' | 'not' | 'max'}_desktopLarge` + | `${'min' | 'not' | 'max'}_desktopHuge`; + + type ColumnsVisibility = Record; + + type TemplateRenderFunction = ( + (h: CreateElement, row: Data, index: number) => JSX.Element | string | null + ); + + type RowClickEventPayload = { row: Data, event: PointerEvent, index: number }; + + type BaseTableOptions = { headings?: Record, initialPage?: number, perPage?: number, @@ -16,14 +35,17 @@ declare module 'vue-tables-2-premium' { filterByColumn?: boolean, columnsDropdown?: boolean, preserveState?: boolean, - columnsDisplay?: Record, // - Voir https://matanya.gitbook.io/vue-tables-2/columns-visibility + saveState?: boolean, + columnsDisplay?: ColumnsVisibility, columnsClasses?: Record, + templates?: Record>, + rowClassCallback(row: Data): VNodeClass, }; - interface BaseTableInstance { + interface BaseTableInstance { name: string; columns: string[]; - data: Array>; + data: Data[]; filtersCount: number; openChildRows: number[]; selectedRows: number[] | undefined; @@ -39,7 +61,7 @@ declare module 'vue-tables-2-premium' { resetCustomFilters(): void; setLoadingState(): void; $refs: { - table: BaseTableInstance, + table: BaseTableInstance, }; } @@ -53,38 +75,40 @@ declare module 'vue-tables-2-premium' { // - Client component specific types // - export type ClientCustomFilter = { + export type ClientCustomFilter = { name: string, - callback(item: TData, identifier: number | string | boolean): boolean, + callback(item: Data, identifier: number | string | boolean): boolean, }; - export type CustomSortFunction = (ascending: boolean) => (a: TData, b: TData) => number; + export type CustomSortFunction = (ascending: boolean) => (a: Data, b: Data) => number; - export type ClientTableOptions = BaseTableOptions & { - initFilters: TFilter, - customSorting?: Record>, - customFilters?: ClientCustomFilter[], + export type ClientTableOptions = BaseTableOptions & { + initFilters: Filters, + customSorting?: Record>, + customFilters?: Array>, }; - export interface ClientTableInstance extends BaseTableInstance { - filteredData: Array>; - allFilteredData: Array>; + export interface ClientTableInstance extends BaseTableInstance { + filteredData: Data[]; + allFilteredData: Data[]; } // // - Server component specific types // - export type RequestFunction = (pagination: PaginationParams) => Promise<{ data: TData } | undefined>; + export type RequestFunction = (pagination: PaginationParams) => ( + Promise<{ data: Data } | undefined> + ); - export type ServerTableOptions = BaseTableOptions & { + export type ServerTableOptions = BaseTableOptions & { customFilters?: string[], - requestFunction?: RequestFunction, + requestFunction?: RequestFunction, }; - export interface ServerTableInstance extends BaseTableInstance { + export interface ServerTableInstance extends BaseTableInstance { setRequestParams(params: Record): void; - getData(): any; + geData(): Data[]; getQueryParams(): Record; } } diff --git a/client/src/globals/types/vendors/vue.d.ts b/client/src/globals/types/vendors/vue.d.ts index 1b60a2dd8..12d0b01ed 100644 --- a/client/src/globals/types/vendors/vue.d.ts +++ b/client/src/globals/types/vendors/vue.d.ts @@ -1,24 +1,34 @@ -declare module 'vue' { - import type { VueConstructor as VueConstructorCore } from 'vue'; +import 'vue'; - export type { VNode } from 'vue'; +declare module 'vue' { + import type { ComponentOptions, VueConstructor } from 'vue'; + import type { DefaultData, DefaultComputed, DefaultMethods } from 'vue/types/options'; export type ComponentRef any> = ( | InstanceType | undefined ); - export interface VueConstructor extends VueConstructorCore { - $router: any; - $route: any; - } + export type RawComponent> = ( + & ComponentOptions, Methods, DefaultComputed, Props> + & VueConstructor + ); + + export type VNodeClass = + | string + | number + | null + | undefined + | boolean + | Record + | VNodeClass[]; } declare module 'vue/types/options' { import type { + VNode, CreateElement, RenderContext, - VNode, ComponentOptions as CoreComponentOptions, } from 'vue'; diff --git a/client/src/globals/types/vendors/vue2-datepicker.d.ts b/client/src/globals/types/vendors/vue2-datepicker.d.ts new file mode 100644 index 000000000..5ed213aa8 --- /dev/null +++ b/client/src/globals/types/vendors/vue2-datepicker.d.ts @@ -0,0 +1,106 @@ +declare module 'vue2-datepicker' { + import type { RawComponent } from 'vue'; + + export type Translations = { + days?: string[], + months?: string[], + yearFormat: string, + monthFormat: string, + monthBeforeYear: boolean, + formatLocale: { + months: string[], + monthsShort: string[], + weekdays: string[], + weekdaysShort: string[], + weekdaysMin: string[], + firstDayOfWeek: number, + firstWeekContainsDate: number, + meridiem(h: number, _: number, isLowercase: boolean): boolean, + meridiemParse: RegExp, + isPM(input: string): boolean, + }, + }; + + export type Shortcuts = { + text: string, + onClick(): any, + }; + + export type TimePickerOptions = { + start: string, + step: string, + end: string, + format: string, + }; + + export type Formatter = { + stringify(date: Date | null | undefined, format: string): string, + parse(value: string | null | undefined, format: string): Date | null, + getWeek?(value: Date, options: { firstDayOfWeek?: number, firstWeekContainsDate?: number }): number, + }; + + export type DatePickerEmit = (value: Date | [Date, Date] | null | [null, null]) => void; + export type DatePickerSlotParams = { emit: DatePickerEmit }; + + const Datepicker: RawComponent<{ + type?: 'date' | 'datetime' | 'year' | 'month' | 'time' | 'week', + range?: boolean, + format?: string, + formatter?: Formatter, + valueType?: 'date' | 'timestamp' | 'format' | string, + defaultValue?: Date, + lang?: Translations, + placeholder?: string, + editable?: boolean, + clearable?: boolean, + confirm?: boolean, + confirmText?: string, + multiple?: boolean, + disabled?: boolean, + disabledDate?(date: Date, currentValue: Date[]): boolean, + disabledTime?(date: Date): boolean, + appendToBody?: boolean, + inline?: boolean, + inputClass?: string, + inputAttr?(): Record, + open?: boolean, + defaultPanel?: 'year' | 'month', + popupStyle?(): Record, + popupClass?: string, + shortcuts?: Shortcuts[], + titleFormat?: string, + partialUpdate?: boolean, + rangeSeparator?: string, + showWeekNumber?: boolean, + hourStep?: number, + minuteStep?: number, + secondStep?: number, + hourOptions?: number[], + minuteOptions?: number[], + secondOptions?: number[], + showHour?: boolean, + showMinute?: boolean, + showSecond?: boolean, + use12h?: boolean, + showTimeHeader?: boolean, + timeTitleFormat?: string, + timePickerOptions?: TimePickerOptions, + prefixClass?: string, + scrollDuration?: number, + }>; + + export default Datepicker; +} + +declare module 'vue2-datepicker/locale/es/fr' { + import type { Translations } from 'vue2-datepicker'; + + const translations: Translations; + export default translations; +} +declare module 'vue2-datepicker/locale/es/en' { + import type { Translations } from 'vue2-datepicker'; + + const translations: Translations; + export default translations; +} diff --git a/client/src/locale/en/common.js b/client/src/locale/en/common.js index a3758c611..edae620f0 100644 --- a/client/src/locale/en/common.js +++ b/client/src/locale/en/common.js @@ -2,7 +2,6 @@ export default { 'hello-name': "Hello {name}!", 'your-settings': "Your settings", 'logout-quit': "Quit Loxya (Robert2)", - 'action-add': "Add", 'action-edit': "Edit", 'action-view': "Display details", @@ -16,10 +15,8 @@ export default { 'action-enable': "Enable", 'action-disable': "Disable", 'action-refresh': "Refresh data", - 'yes': "Yes", 'no': "No", - 'warning': "Warning!", 'loading': "Loading...", 'please-confirm': "Please confirm...", @@ -43,7 +40,6 @@ export default { 'take-control': "Take control", 'update-in-progress': "Update in progress", 'regenerate-link': "Regenerate link", - 'please-choose': "Please choose...", 'start-typing-to-search': "Start typing to search...", 'type-at-least-count-chars-to-search': [ @@ -63,7 +59,8 @@ export default { 'optional': "Optional", 'n-persons': ["{count} person", "{count} persons"], 'and-n-others': ["and {count} other", "and {count} others"], - + 'add-comment': "Add a comment", + 'modify-comment': "Modify comment", 'save': "Save", 'manually-save': "Manually save", 'save-draft': "Save draft", @@ -109,9 +106,7 @@ export default { 'admin': "Administrator", 'member': "Member", 'visitor': "Visitor", - 'external': "External", 'owner': "Owner", - 'owner-key': "Owner:", 'opening-hours': "Opening hours", 'hours': "hours", 'minutes': "minutes", @@ -120,9 +115,9 @@ export default { 'ref': "Ref.", 'ref-ref': "Ref.: {reference}", 'reference': "Reference", + 'number': "Number", 'park': "Park", 'prices': "Prices", - 'identification-type': "Identification type", 'rental-price': "Rental price", 'replacement-price': "Replacement price", 'rent-price': "Rent. price", @@ -130,6 +125,7 @@ export default { 'value-per-day': '{value}\u00A0/\u00A0day', 'serial-number': "Serial n°", 'examples-list': "Examples: {list}, etc.", + 'not-specified': "Not specified", 'qty': "Qty", 'stock-qty': "Stock qty", @@ -171,6 +167,7 @@ export default { 'event-details': "Event's details", 'title': "Title", + 'date': "Date", 'dates': "Dates", 'start-end-dates': "Start and end dates", 'start-date': "Start Date", @@ -181,7 +178,6 @@ export default { "Duration {duration} day", "Duration {duration} days", ], - 'please-choose-dates': "Please choose dates", 'confirmed': "Confirmed", 'not-confirmed': "Not confirmed", 'is-billable': "Is billable?", @@ -193,8 +189,6 @@ export default { 'unconfirm-event': "Set event back on hold", 'delete-event': "Delete event", 'duplicate-event': "Duplicate event", - 'duplicate-the-event': "Duplicate event \"{title}\"", - 'dates-of-duplicated-event': "Dates of new event", 'print': "Print", 'print-summary': "Print this summary", 'open': "Open", @@ -306,9 +300,11 @@ export default { 'add-tags': "Add tags", 'remove-all-tags': "Remove all tags", 'remaining-count': "{count} remaining", + 'departure-inventory': "Departure inventory", 'return-inventory': "Return inventory", 'grouped-by': "Display grouped by:", 'not-grouped': "Not grouped", + 'start-on': "Start on", 'return-scheduled-on': "Return scheduled on", 'back-to-calendar': "Back to calendar", 'previous-month': "Previous month", @@ -316,31 +312,16 @@ export default { 'used-by': "Used by", 'events-count': ['{count} event', '{count} events'], - 'reservations-count': ['{count} reservation', '{count} reservations'], - 'list-templates-count': ['{count} list template', '{count} list templates'], - 'use-list-template': "Use a template...", - 'choose-list-template-to-use': "Choose a template to use", 'use': "Use", - 'list-template-details': "Details of list template \"{name}\"", - 'list-template-use-warning': "Be careful, using this template will add its materials to the already selected, and will save the list right away!", 'use-this-template': "Use this list template", - 'no-list-template-available': ( - "No list template available yet...\n" + - "You can create them from the \"Materials\" page, then \"List templates\" button." - ), - 'create-list-template': "Create a material list template", - 'create-list-template-from-event': "Create a list template with this event", - 'list-template-created': "Le modèle de liste de matériel nommé «\u00A0{name}\u00A0» a bien été créé", + 'create-company': "Add a new company", 'inventories': "Inventories", 'terminate-inventory': "Terminate inventory", - 'warning-terminate-inventory': "Beware, once this inventory terminated, it will no longer be modifiable.", 'inventory-validation-error': "Some quantities are not valid. Please double-check the list.", - 'list-templates': "List templates", - 'materials-list-templates': "Materials list templates", 'reuse-list-from-event': "Add materials from another event...", 'choose-event-to-reuse-materials-list': "Choose an event to reuse its materials list", 'type-to-search-event': "Type in to search an event...", @@ -359,6 +340,7 @@ export default { 'events': "Events", 'user': "User", 'beneficiary': "Beneficiary", + 'main-beneficiary': "Main Beneficiary", 'borrower': "Borrower", 'technicians': "Technicians", 'material': "Material", diff --git a/client/src/locale/fr/common.js b/client/src/locale/fr/common.js index 3ac5588af..d6da238e2 100644 --- a/client/src/locale/fr/common.js +++ b/client/src/locale/fr/common.js @@ -2,7 +2,6 @@ export default { 'hello-name': "Bonjour {name}\u00A0!", 'your-settings': "Vos paramètres", 'logout-quit': "Quitter Loxya (Robert2)", - 'action-add': "Ajouter", 'action-edit': "Modifier", 'action-view': "Afficher en détail", @@ -16,10 +15,8 @@ export default { 'action-enable': "Activer", 'action-disable': "Désactiver", 'action-refresh': "Rafraîchir les données", - 'yes': "Oui", 'no': "Non", - 'warning': "Attention\u00A0!", 'loading': "Chargement en cours...", 'please-confirm': "Veuillez confirmer...", @@ -43,7 +40,6 @@ export default { 'take-control': "Prendre la main", 'update-in-progress': "Modification en cours", 'regenerate-link': "Re-générer le lien", - 'please-choose': "Veuillez choisir...", 'start-typing-to-search': "Commencez à écrire pour rechercher...", 'type-at-least-count-chars-to-search': [ @@ -63,7 +59,8 @@ export default { 'optional': "Optionnel", 'n-persons': ["{count} personne", "{count} personnes"], 'and-n-others': ["et {count} autre", "et {count} autres"], - + 'add-comment': "Ajouter un commentaire", + 'modify-comment': "Modifier le commentaire", 'save': "Sauvegarder", 'manually-save': "Sauvegarder manuellement", 'save-draft': "Sauvegarder le brouillon", @@ -109,9 +106,7 @@ export default { 'admin': "Administrateur", 'member': "Membre", 'visitor': "Visiteur", - 'external': "Externe", 'owner': "Propriétaire", - 'owner-key': "Propriétaire\u00A0:", 'opening-hours': "Horaires d'ouverture", 'hours': "heures", 'minutes': "minutes", @@ -120,9 +115,9 @@ export default { 'ref': "Réf.", 'ref-ref': "Ref.\u00A0: {reference}", 'reference': "Référence", + 'number': "Numéro", 'park': "Parc", 'prices': "Tarifs", - 'identification-type': "Type d'identification", 'rental-price': "Tarif location", 'replacement-price': "Prix de remplacement", 'rent-price': "Tarif loc.", @@ -130,6 +125,7 @@ export default { 'value-per-day': '{value}\u00A0/\u00A0jour', 'serial-number': "N° de série", 'examples-list': "Exemples\u00A0: {list}, etc.", + 'not-specified': "Non renseigné", 'qty': "Qté", 'stock-qty': "Qté stock", @@ -170,6 +166,7 @@ export default { 'event-details': "Détails de l'événement", 'title': "Titre", + 'date': "Date", 'dates': "Dates", 'start-end-dates': "Dates de début et fin", 'start-date': "Date de début", @@ -180,7 +177,6 @@ export default { "Durée {duration} jour", "Durée {duration} jours", ], - 'please-choose-dates': "Veuillez choisir les dates", 'confirmed': "Confirmé", 'not-confirmed': "Non confirmé", 'is-billable': "Est facturable\u00A0?", @@ -192,8 +188,6 @@ export default { 'unconfirm-event': "Remettre l'événement en attente", 'delete-event': "Supprimer l'événement", 'duplicate-event': "Dupliquer l'événement", - 'duplicate-the-event': "Dupliquer l'événement «\u00A0{title}\u00A0»", - 'dates-of-duplicated-event': "Dates du nouvel événement", 'print': "Imprimer", 'print-summary': "Imprimer ce récapitulatif", 'open': "Ouvrir", @@ -305,9 +299,11 @@ export default { 'add-tags': "Ajouter des tags", 'remove-all-tags': "Enlever tous les tags", 'remaining-count': "reste {count}", + 'departure-inventory': "Inventaire de départ", 'return-inventory': "Inventaire de retour", 'grouped-by': "Voir groupé par\u00A0:", 'not-grouped': "Non groupé", + 'start-on': "Débute le", 'return-scheduled-on': "Retour prévu le", 'back-to-calendar': "Retour au calendrier", 'previous-month': "Mois précédent", @@ -316,30 +312,16 @@ export default { 'used-by': "Utilisé dans", 'events-count': ['{count} événement', '{count} événements'], 'reservations-count': ['{count} réservation', '{count} réservations'], - 'list-templates-count': ['{count} modèle de liste', '{count} modèles de liste'], - 'use-list-template': "Utiliser un modèle...", - 'choose-list-template-to-use': "Choisir un modèle de liste à utiliser", 'use': "Utiliser", - 'list-template-details': "Détails du modèle de liste «\u00A0{name}\u00A0»", - 'list-template-use-warning': "Attention, utiliser ce modèle de liste va ajouter le matériel à celui déjà sélectionné, et sauvegarder la liste du matériel tout de suite après\u00A0!", 'use-this-template': "Utiliser ce modèle de liste", - 'no-list-template-available': ( - "Aucun modèle de liste disponible pour le moment...\n" + - "Vous pouvez en créer depuis la page \"Matériel\" puis \"Modèles de liste\"." - ), - 'create-list-template': "Créer un modèle de liste de matériel", - 'create-list-template-from-event': "Créer un modèle de liste avec cet événement", - 'list-template-created': "Le modèle de liste de matériel nommé «\u00A0{name}\u00A0» a bien été créé", + 'create-company': "Ajouter une nouvelle société", 'inventories': "Inventaires", 'terminate-inventory': "Terminer l'inventaire", - 'warning-terminate-inventory': "Attention, une fois cet inventaire terminé, il ne sera plus modifiable.", 'inventory-validation-error': "Certaines quantités ne sont pas correctes. Veuillez vérifier à nouveau la liste.", - 'list-templates': "Modèles de liste", - 'materials-list-templates': "Modèles de liste de matériel", 'reuse-list-from-event': "Ajouter du matériel depuis un autre événement...", 'choose-event-to-reuse-materials-list': "Choisir un événement pour réutiliser sa liste de matériel", 'type-to-search-event': "Entrez le nom d'un événement...", @@ -358,6 +340,7 @@ export default { 'events': "Événements", 'user': "Utilisateur", 'beneficiary': "Bénéficiaire", + 'main-beneficiary': "Bénéficiaire principal", 'borrower': "Emprunteur", 'technicians': "Techniciens", 'material': "Matériel", diff --git a/client/src/stores/api/@codes.ts b/client/src/stores/api/@codes.ts index f5ec2c6fb..02d9bed3e 100644 --- a/client/src/stores/api/@codes.ts +++ b/client/src/stores/api/@codes.ts @@ -20,4 +20,14 @@ export enum ApiErrorCode { /** Le payload fourni dans la requête ne doit pas être vide. */ EMPTY_PAYLOAD = 401, + + // + // - Conflits. + // + + /** + * Un conflit dû au fait qu'une tentative d'assignation d'un + * technicien a échoué vu qu'il est déjà mobilisé à ce moment. + */ + TECHNICIAN_ALREADY_BUSY = 201, } diff --git a/client/src/stores/api/@types.ts b/client/src/stores/api/@types.ts index c6276a1a5..5f2bb769b 100644 --- a/client/src/stores/api/@types.ts +++ b/client/src/stores/api/@types.ts @@ -1,8 +1,54 @@ +/** Sens de tri. */ +export enum Direction { + /** Direction ascendante. */ + ASC = 'asc', + + /** Direction descendante. */ + DESC = 'desc', +} + export type SortableParams = { + /** La colonne avec laquelle on veut trier le jeu de résultats. */ orderBy?: string, + + /** + * Le jeu de résultat doit-il être trié de manière ascendante selon la colonne choisie + * ci-dessus (ou celle par défaut si aucun colonne n'a été explicitement choisie). + * + * - Si `1`, le jeu de résultat sera trié de manière ascendante. + * - Si `0`, il sera trié de manière descendante. + * + */ + // TODO: Modifier ça pour quelque chose du genre : `{ direction: Direction }`. + // (il faudra adapter le component de tableau qui utilise cette notation obsolète). ascending?: 0 | 1, }; +export type PaginationParams = { + /** + * La page dont on veut récupérer le résultat. + * + * @default 1 + */ + page?: number, + + /** Le nombre de résultats par page que l'on souhaite récupérer. */ + limit?: number, +}; + +export type ListingParams = ( + & { + /** + * Permet de rechercher un terme en particulier. + * + * @default undefined + */ + search?: string, + } + & SortableParams + & PaginationParams +); + export type PaginatedData = { data: T, pagination: { @@ -15,20 +61,35 @@ export type PaginatedData = { }, }; -export type PaginationParams = { - page: number, - limit?: number, +export type WithCount = { + count: number, + data: T, }; -export type ListingParams = ( - & { search?: string } - & SortableParams - & PaginationParams -); +// - Types liés aux imports -export type FormErrorDetail = Record; +export type CsvDelimiter = ',' | ';' | ':' | `\t`; -export type WithCount = { - count: number, - data: T, +export type CsvImport> = { + mapping: Mapping, + delimiter: CsvDelimiter, + file: File, +}; + +export type CsvColumnError> = { + field: keyof Mapping, + value: string, + error: string, +}; + +export type CsvImportError> = { + line: number, + message: string, + errors: Array>, +}; + +export type CsvImportResults> = { + total: number, + success: number, + errors: Array>, }; diff --git a/client/src/stores/api/beneficiaries.ts b/client/src/stores/api/beneficiaries.ts index a3a0754d3..0795fdb72 100644 --- a/client/src/stores/api/beneficiaries.ts +++ b/client/src/stores/api/beneficiaries.ts @@ -1,14 +1,30 @@ +import moment from 'moment'; import requester from '@/globals/requester'; +import { normalize as normalizeEstimate } from '@/stores/api/estimates'; +import { normalize as normalizeInvoice } from '@/stores/api/invoices'; +import type { MomentInput } from 'moment'; import type { Company } from '@/stores/api/companies'; import type { Country } from '@/stores/api/countries'; import type { User } from '@/stores/api/users'; -import type { PaginatedData, ListingParams } from './@types'; +import type { RawEstimate, Estimate } from '@/stores/api/estimates'; +import type { RawInvoice, Invoice } from '@/stores/api/invoices'; +import type { BookingSummary } from '@/stores/api/bookings'; +import type { + Direction, + ListingParams, + PaginatedData, + PaginationParams, +} from './@types'; // // - Types // +export type BeneficiaryStats = { + borrowings: number, +}; + export type Beneficiary = { id: number, first_name: string, @@ -24,8 +40,11 @@ export type Beneficiary = { locality: string | null, country_id: number | null, country: Country | null, + full_address: string | null, note: string | null, user_id: number | null, + can_make_reservation: boolean, + stats: BeneficiaryStats, }; export type BeneficiaryDetails = Beneficiary & { @@ -46,7 +65,33 @@ export type BeneficiaryEdit = { note: string | null, }; -type GetAllParams = ListingParams & { deleted?: boolean }; +type GetAllParams = ListingParams & { + /** + * Permet de ne récupérer que les bénéficiaires dans la "corbeille". + * + * @default false + */ + deleted?: boolean, +}; + +type GetBookingsParams = PaginationParams & { + /** + * Date à partir de laquelle on veut la liste des bookings. + * Inclura aussi les bookings qui ont commencés avant mais se terminent après cette date. + * + * @default undefined + */ + after?: MomentInput, + + /** + * Le sens dans lequel on veut récupérer les bookings : + * - `Direction.ASC`: Du plus ancien au plus récent. + * - `Direction.DESC`: Du plus récent au plus ancien. + * + * @default Direction.DESC + */ + direction?: Direction, +}; // // - Fonctions @@ -76,4 +121,35 @@ const remove = async (id: Beneficiary['id']): Promise => { await requester.delete(`/beneficiaries/${id}`); }; -export default { all, one, create, update, restore, remove }; +const bookings = async ( + id: Beneficiary['id'], + { after, ...otherParams }: GetBookingsParams = {}, +): Promise> => { + const params: Record = { ...otherParams }; + if (after !== undefined) { + params.after = moment(after).format(); + } + return (await requester.get(`/beneficiaries/${id}/bookings`, { params })).data; +}; + +const estimates = async (id: Beneficiary['id']): Promise => { + const rawEstimates: RawEstimate[] = (await requester.get(`/beneficiaries/${id}/estimates`)).data; + return rawEstimates.map(normalizeEstimate); +}; + +const invoices = async (id: Beneficiary['id']): Promise => { + const rawInvoices: RawInvoice[] = (await requester.get(`/beneficiaries/${id}/invoices`)).data; + return rawInvoices.map(normalizeInvoice); +}; + +export default { + all, + one, + create, + update, + restore, + remove, + bookings, + estimates, + invoices, +}; diff --git a/client/src/stores/api/bookings.ts b/client/src/stores/api/bookings.ts index 9e952e966..8318a1031 100644 --- a/client/src/stores/api/bookings.ts +++ b/client/src/stores/api/bookings.ts @@ -35,6 +35,7 @@ type EventBookingSummary = ( | 'is_confirmed' | 'is_billable' | 'is_archived' + | 'is_departure_inventory_done' | 'is_return_inventory_done' | 'has_missing_materials' | 'has_not_returned_materials' diff --git a/client/src/stores/api/companies.ts b/client/src/stores/api/companies.ts index bb3916d7d..c7d3d50f4 100644 --- a/client/src/stores/api/companies.ts +++ b/client/src/stores/api/companies.ts @@ -16,6 +16,7 @@ export type Company = { locality: string | null, country_id: Country['id'] | null, country: Country | null, + full_address: string | null, note: string | null, }; diff --git a/client/src/stores/api/events.ts b/client/src/stores/api/events.ts index a6a3f6b84..c1a4c2989 100644 --- a/client/src/stores/api/events.ts +++ b/client/src/stores/api/events.ts @@ -23,8 +23,10 @@ export type EventMaterial = ( pivot: { quantity: number, quantity_missing: number, - quantity_returned: number, - quantity_returned_broken: number, + quantity_departed: number | null, + quantity_returned: number | null, + quantity_returned_broken: number | null, + departure_comment: string | null, }, } ); @@ -40,7 +42,10 @@ export type RawEvent< description: string | null, start_date: string, end_date: string, - duration: number, // - En jours. + duration: { + days: number, + hours: number, + }, color: string | null, location: string | null, total_replacement: DecimalType, @@ -49,13 +54,37 @@ export type RawEvent< technicians: Technician[], materials: EventMaterial[], is_confirmed: boolean, - is_return_inventory_started: boolean, - is_return_inventory_done: boolean, note: string | null, author: User | null, created_at: string, updated_at: string, } + & ( + | { + is_departure_inventory_done: true, + departure_inventory_datetime: string | null, + departure_inventory_author: User | null, + } + | { + is_departure_inventory_done: false, + departure_inventory_datetime: null, + departure_inventory_author: null, + } + ) + & ( + | { + is_return_inventory_done: true, + is_return_inventory_started: true, + return_inventory_datetime: string | null, + return_inventory_author: User | null, + } + | { + is_return_inventory_done: false, + is_return_inventory_started: boolean, + return_inventory_datetime: null, + return_inventory_author: null, + } + ) & ( | { is_archived: true, @@ -118,6 +147,14 @@ type EventReturnInventoryMaterial = { type EventReturnInventory = EventReturnInventoryMaterial[]; +type EventDepartureInventoryMaterial = { + id: Material['id'], + actual: number, + comment?: string | null, +}; + +type EventDepartureInventory = EventDepartureInventoryMaterial[]; + type EventDuplicatePayload = { start_date: string, end_date: string, @@ -205,6 +242,14 @@ const finishReturnInventory = async (id: Event['id'], inventory: EventReturnInve normalize((await requester.put(`/events/${id}/return/finish`, inventory)).data) ); +const updateDepartureInventory = async (id: Event['id'], inventory: EventDepartureInventory): Promise => ( + normalize((await requester.put(`/events/${id}/departure`, inventory)).data) +); + +const finishDepartureInventory = async (id: Event['id'], inventory: EventDepartureInventory): Promise => ( + normalize((await requester.put(`/events/${id}/departure/finish`, inventory)).data) +); + const createInvoice = async (id: Event['id'], discountRate: number = 0): Promise => ( normalizeInvoice((await requester.post(`/events/${id}/invoices`, { discountRate })).data) ); @@ -221,9 +266,14 @@ const update = async (id: Event['id'], params: any): Promise => ( normalize((await requester.put(`/events/${id}`, params)).data) ); -const duplicate = async (id: Event['id'], data: EventDuplicatePayload): Promise => ( - normalize((await requester.post(`/events/${id}/duplicate`, data)).data) -); +const duplicate = async ( + id: Event['id'], + data: EventDuplicatePayload, + force: boolean = false, +): Promise => { + const params = { force: force || undefined }; + return normalize((await requester.post(`/events/${id}/duplicate`, data, { params })).data); +}; const remove = async (id: Event['id']): Promise => { await requester.delete(`/events/${id}`); @@ -247,6 +297,8 @@ export default { unarchive, updateReturnInventory, finishReturnInventory, + updateDepartureInventory, + finishDepartureInventory, createInvoice, createEstimate, create, diff --git a/client/src/stores/api/groups.ts b/client/src/stores/api/groups.ts index 9f05993fc..cf24523b1 100644 --- a/client/src/stores/api/groups.ts +++ b/client/src/stores/api/groups.ts @@ -38,4 +38,9 @@ const all = (): GroupDetails[] => { ]; }; -export default { all }; +const one = (group: Group): GroupDetails | undefined => { + const allGroups = all(); + return allGroups.find(({ id }: GroupDetails) => id === group); +}; + +export default { all, one }; diff --git a/client/src/stores/api/materials.ts b/client/src/stores/api/materials.ts index 7d3720442..f7598ea95 100644 --- a/client/src/stores/api/materials.ts +++ b/client/src/stores/api/materials.ts @@ -54,7 +54,7 @@ export type MaterialWithAvailabilities = Material & { available_quantity?: number, }; -export type BookingWithPivot = BookingSummary & { +export type MaterialBookingSummary = BookingSummary & { pivot: { quantity: number, }, @@ -91,7 +91,7 @@ type BaseFilters = { }; export type Filters = Omit & { - dateForQuantities?: string | { start: string, end: string }, + quantitiesPeriod?: { start: string, end: string }, category?: Category['id'] | typeof UNCATEGORIZED, park?: Park['id'], tags?: Array, @@ -107,21 +107,21 @@ type GetAllRaw = GetAllBase & { paginated: false }; async function all(params: GetAllRaw): Promise; async function all(params: GetAllPaginated): Promise>; -async function all({ dateForQuantities, ...params }: GetAllPaginated | GetAllRaw): Promise { - const rawParams: Record = params; - if (dateForQuantities !== undefined) { - if ( - typeof dateForQuantities === 'object' && - 'start' in dateForQuantities && - 'end' in dateForQuantities - ) { - rawParams['dateForQuantities[start]'] = dateForQuantities.start; - rawParams['dateForQuantities[end]'] = dateForQuantities.end; - } else { - rawParams.dateForQuantities = dateForQuantities; +async function all({ quantitiesPeriod, ...otherParams }: GetAllPaginated | GetAllRaw): Promise { + const params: Record = otherParams; + if (quantitiesPeriod !== undefined) { + const isValidPeriod = ( + typeof quantitiesPeriod === 'object' && + 'start' in quantitiesPeriod && + 'end' in quantitiesPeriod + ); + if (!isValidPeriod) { + throw new Error('Invalid quantities period.'); } + params['quantitiesPeriod[start]'] = quantitiesPeriod.start; + params['quantitiesPeriod[end]'] = quantitiesPeriod.end; } - return (await requester.get('/materials', { params: rawParams })).data; + return (await requester.get('/materials', { params })).data; } /* eslint-enable func-style */ @@ -151,9 +151,13 @@ const remove = async (id: Material['id']): Promise => { await requester.delete(`/materials/${id}`); }; -const bookings = async (id: Material['id']): Promise => ( - (await requester.get(`/materials/${id}/bookings`)).data -); +const bookings = async ( + id: Material['id'], + params?: PaginationParams, +): Promise> => { + const config = { ...(params ? { params } : {}) }; + return (await requester.get(`/materials/${id}/bookings`, config)).data; +}; const documents = async (id: Material['id']): Promise => ( (await requester.get(`/materials/${id}/documents`)).data diff --git a/client/src/stores/api/session.ts b/client/src/stores/api/session.ts index 1b59e6ca5..6c37b7244 100644 --- a/client/src/stores/api/session.ts +++ b/client/src/stores/api/session.ts @@ -19,7 +19,7 @@ export enum AppContext { // - Types // -type Session = UserDetails & { +export type Session = UserDetails & { language: string, }; @@ -27,7 +27,7 @@ type NewSession = Session & { token: string, }; -type Credentials = { +export type Credentials = { identifier: string, password: string, context?: AppContext, diff --git a/client/src/stores/api/settings.ts b/client/src/stores/api/settings.ts index d429b24b7..cf023d93b 100644 --- a/client/src/stores/api/settings.ts +++ b/client/src/stores/api/settings.ts @@ -1,10 +1,17 @@ import requester from '@/globals/requester'; +import type { Merge } from 'type-fest'; + // // - Types // -export type MaterialDisplayMode = 'categories' | 'sub-categories' | 'parks' | 'flat'; +export enum MaterialDisplayMode { + CATEGORIES = 'categories', + SUB_CATEGORIES = 'sub-categories', + PARKS = 'parks', + FLAT = 'flat', +} export enum ReturnInventoryMode { START_EMPTY = 'start-empty', @@ -25,12 +32,23 @@ export type Settings = { showLocation: boolean, showBorrower: boolean, }, + public: ( + | { enabled: true, url: string } + | { enabled: false } + ), }, returnInventory: { mode: ReturnInventoryMode, }, }; +type SettingsEdit = Partial + & { public: { enabled: boolean } } + ), +}>>; + // // - Fonctions // @@ -39,7 +57,7 @@ const all = async (): Promise => ( (await requester.get('/settings')).data ); -const update = async (data: Partial): Promise => ( +const update = async (data: SettingsEdit): Promise => ( (await requester.put('/settings', data)).data ); diff --git a/client/src/stores/api/technicians.ts b/client/src/stores/api/technicians.ts index 2cb9a80ad..3bb4d832b 100644 --- a/client/src/stores/api/technicians.ts +++ b/client/src/stores/api/technicians.ts @@ -41,15 +41,31 @@ export type TechnicianEdit = { note: string | null, }; -type GetAllParams = ListingParams & { deleted?: boolean }; +type GetAllParams = ListingParams & { + availabilityPeriod?: { start: string, end: string }, + deleted?: boolean, +}; // // - Fonctions // -const all = async (params: GetAllParams): Promise> => ( - (await requester.get('/technicians', { params })).data -); +const all = async ({ availabilityPeriod, ...otherParams }: GetAllParams): Promise> => { + const params: Record = otherParams; + if (availabilityPeriod !== undefined) { + const isValidPeriod = ( + typeof availabilityPeriod === 'object' && + 'start' in availabilityPeriod && + 'end' in availabilityPeriod + ); + if (!isValidPeriod) { + throw new Error('Invalid quantities period.'); + } + params['availabilityPeriod[start]'] = availabilityPeriod.start; + params['availabilityPeriod[end]'] = availabilityPeriod.end; + } + return (await requester.get('/technicians', { params })).data; +}; const allWhileEvent = async (eventId: Event['id']): Promise => ( (await requester.get(`/technicians/while-event/${eventId}`)).data diff --git a/client/src/stores/auth.js b/client/src/stores/auth.ts similarity index 53% rename from client/src/stores/auth.js rename to client/src/stores/auth.ts index 27755665b..21ae5e71f 100644 --- a/client/src/stores/auth.js +++ b/client/src/stores/auth.ts @@ -1,12 +1,21 @@ import HttpCode from 'status-code-enum'; import config from '@/globals/config'; import cookies from '@/utils/cookies'; +import { isRequestErrorStatusCode } from '@/utils/errors'; import apiSession from '@/stores/api/session'; -const setSessionCookie = (token) => { +import type { Module, ActionContext } from 'vuex'; +import type { Session, Credentials } from '@/stores/api/session'; +import type { Group } from '@/stores/api/groups'; + +export type State = { + user: Session | null, +}; + +const setSessionCookie = (token: string): void => { const { cookie, timeout } = config.auth; - const cookieConfig = {}; + const cookieConfig: Cookies.CookieAttributes = {}; if (timeout) { const timeoutMs = timeout * 60 * 60 * 1000; const timeoutDate = new Date(Date.now() + timeoutMs); @@ -16,15 +25,15 @@ const setSessionCookie = (token) => { cookies.set(cookie, token, cookieConfig); }; -export default { +const store: Module = { namespaced: true, state: { user: null, }, getters: { - isLogged: (state) => !!state.user, + isLogged: (state: State) => !!state.user, - is: (state) => (groups) => { + is: (state: State) => (groups: Group | Group[]) => { if (!state.user) { return false; } @@ -34,20 +43,20 @@ export default { }, }, mutations: { - setUser(state, user) { + setUser(state: State, user: Session) { state.user = user; }, - updateUser(state, newData) { + updateUser(state: State, newData: Session) { state.user = { ...state.user, ...newData }; }, - setLocale(state, language) { - state.user.language = language; + setLocale(state: State, language: string) { + state.user!.language = language; }, }, actions: { - async fetch({ dispatch, commit }) { + async fetch({ dispatch, commit }: ActionContext) { if (!cookies.get(config.auth.cookie)) { commit('setUser', null); return; @@ -57,16 +66,16 @@ export default { commit('setUser', await apiSession.get()); } catch (error) { // - Non connecté. - if (error.httpCode === HttpCode.ClientErrorUnauthorized) { - dispatch('logout'); + if (isRequestErrorStatusCode(error, HttpCode.ClientErrorUnauthorized)) { + dispatch('logout', false); } else { // eslint-disable-next-line no-console - console.error('Error:', error.message || error.code); + console.error('Unexpected error during user retrieval:', error); } } }, - async login({ dispatch, commit }, credentials) { + async login({ dispatch, commit }: ActionContext, credentials: Credentials) { const { token, ...user } = await apiSession.create(credentials); commit('setUser', user); setSessionCookie(token); @@ -76,15 +85,27 @@ export default { await dispatch('settings/fetch', undefined, { root: true }); }, - async logout() { - cookies.remove(config.auth.cookie); - window.location.assign(`${config.baseUrl}/login`); + async logout(_: ActionContext, full: boolean = true) { + const hasPotentiallyStatefulSession = !!( + config.auth.isCASEnabled || + config.auth.isSAML2Enabled + ); + const theme = ''; + + if (hasPotentiallyStatefulSession && full) { + window.location.assign(`${config.baseUrl}${theme}/logout`); + } else { + cookies.remove(config.auth.cookie); + window.location.assign(`${config.baseUrl}${theme}/login`); + } // - Timeout de 5 secondes avant de rejeter la promise. // => L'idée étant que la redirection doit avoir lieu dans ce laps de temps. // => Cela permet aussi de "bloquer" les listeners de cette méthode pour éviter // qu'ils exécutent des process post-logout (redirection, vidage de store ...) - await new Promise((_, reject) => { setTimeout(reject, 5000); }); + await new Promise((__: any, reject: any) => { setTimeout(reject, 5000); }); }, }, }; + +export default store; diff --git a/client/src/stores/settings.js b/client/src/stores/settings.js deleted file mode 100644 index 7347eb046..000000000 --- a/client/src/stores/settings.js +++ /dev/null @@ -1,39 +0,0 @@ -import apiSettings from '@/stores/api/settings'; - -const getDefaults = () => ({ - 'eventSummary': { - 'customText': { - 'title': null, - 'content': null, - }, - 'materialDisplayMode': 'sub-categories', - 'showLegalNumbers': true, - }, - 'calendar': { - 'event': { - 'showLocation': true, - 'showBorrower': false, - }, - }, -}); - -export default { - namespaced: true, - state: getDefaults(), - mutations: { - reset(state) { - Object.assign(state, getDefaults()); - }, - set(state, data) { - Object.assign(state, data); - }, - }, - actions: { - reset({ commit }) { - commit('reset'); - }, - async fetch({ commit }) { - commit('set', await apiSettings.all()); - }, - }, -}; diff --git a/client/src/stores/settings.ts b/client/src/stores/settings.ts new file mode 100644 index 000000000..e471412d3 --- /dev/null +++ b/client/src/stores/settings.ts @@ -0,0 +1,52 @@ +import apiSettings, { MaterialDisplayMode, ReturnInventoryMode } from '@/stores/api/settings'; + +import type { Module, ActionContext } from 'vuex'; +import type { Settings } from '@/stores/api/settings'; + +export type State = Settings; + +const getDefaults = (): Settings => ({ + eventSummary: { + customText: { + title: null, + content: null, + }, + materialDisplayMode: MaterialDisplayMode.SUB_CATEGORIES, + showLegalNumbers: true, + }, + calendar: { + event: { + showLocation: true, + showBorrower: false, + }, + public: { + enabled: false, + }, + }, + returnInventory: { + mode: ReturnInventoryMode.START_EMPTY, + }, +}); + +const store: Module = { + namespaced: true, + state: getDefaults(), + mutations: { + reset(state: State) { + Object.assign(state, getDefaults()); + }, + set(state: State, data: State) { + Object.assign(state, data); + }, + }, + actions: { + reset({ commit }: ActionContext) { + commit('reset'); + }, + async fetch({ commit }: ActionContext) { + commit('set', await apiSettings.all()); + }, + }, +}; + +export default store; diff --git a/client/src/themes/default/components/Button/_variables.scss b/client/src/themes/default/components/Button/_variables.scss index 41a097fd3..19a9b875d 100644 --- a/client/src/themes/default/components/Button/_variables.scss +++ b/client/src/themes/default/components/Button/_variables.scss @@ -4,7 +4,7 @@ /// Padding vertical des boutons. /// @type Number -$padding-y: 0.396rem !default; +$padding-y: 0.45rem !default; /// Padding horizontal des boutons. /// @type Number @@ -20,7 +20,7 @@ $border-width: 2px !default; /// Border radius des boutons. /// @type Number -$border-radius: 4px !default; +$border-radius: globals.$border-radius-normal !default; // // - Tailles diff --git a/client/src/themes/default/components/Button/index.scss b/client/src/themes/default/components/Button/index.scss index 656b30451..8a98e94e9 100644 --- a/client/src/themes/default/components/Button/index.scss +++ b/client/src/themes/default/components/Button/index.scss @@ -13,8 +13,9 @@ border: $border-width solid; border-radius: $border-radius; font-size: 1rem; - line-height: 1.25; + line-height: 1; text-decoration: none; + white-space: nowrap; user-select: none; cursor: pointer; transition: all 300ms; @@ -102,8 +103,7 @@ background: map.get($variant, background); color: map.get($variant, color); - &:hover, - &:focus { + &:hover { border-color: map.get($variant, focused-border-color); background-color: map.get($variant, focused-background); color: map.get($variant, focused-color); diff --git a/client/src/themes/default/components/Button/index.tsx b/client/src/themes/default/components/Button/index.tsx index d3fcb70cc..db0e830ac 100644 --- a/client/src/themes/default/components/Button/index.tsx +++ b/client/src/themes/default/components/Button/index.tsx @@ -8,7 +8,7 @@ import type { TooltipOptions } from 'v-tooltip'; import type { PropType } from '@vue/composition-api'; import type { Props as IconProps, Variant } from '@/themes/default/components/Icon'; -const TYPES = [ +export const TYPES = [ 'default', 'success', 'warning', 'danger', 'primary', 'secondary', 'transparent', ] as const; @@ -42,12 +42,13 @@ const PREDEFINED_TYPES = { }, } as const; -type PredefinedType = keyof typeof PREDEFINED_TYPES; -type Type = (typeof TYPES)[number]; +export type PredefinedType = keyof typeof PREDEFINED_TYPES; +export type Type = (typeof TYPES)[number]; type IconName = string | `${string}:${Variant}`; type IconPosition = 'before' | 'after'; type IconOptions = { name: IconName, position?: IconPosition }; +export type IconLoose = IconName | IconOptions; type Props = { /** @@ -83,7 +84,7 @@ type Props = { * - `wrench:solid` * - `{ name: 'wrench:solid', position: 'after' }` */ - icon?: IconName | IconOptions, + icon?: IconLoose, /** * Le type de ` + ); + }; + + const withSidebar = withSnippets && !disabled && snippets.length > 0; + const renderSidebar = (changeDate: DatePickerEmit): JSX.Element | null => { + if (!withSidebar) { + return null; + } + + return ( +
+
+ {snippets.map((snippetGroup: Snippet[], index: number) => ( +
+ {snippetGroup.map((snippet: Snippet) => ( +
{ changeDate(snippet.period); }} + class={['Datepicker__snippet', { + 'Datepicker__snippet--active': snippet.isActive, + }]} + > +
{snippet.label}
+
{snippet.periodLabel}
+
+ ))} +
+ ))} +
+
+ ); + }; + + const renderHiddenInput = (): JSX.Element | null => { + if (!name || disabled) { + return null; + } + + const renderDates = (): JSX.Element => { + if (range) { + const [start, end] = value as Value; + + return ( + + + + + ); + } + + return ) ?? ''} />; + }; + + return ( + + {renderDates()} + {withFullDaysToggle && ( + + )} + + ); + }; + + const className = ['Datepicker', { + 'Datepicker--invalid': invalid, + }]; + + return ( +
+ renderHeader() } : {}), + ...(withFooter ? { footer: () => renderFooter() } : {}), + ...(withSidebar ? { + sidebar: ({ emit: changeDate }: DatePickerSlotParams) => ( + renderSidebar(changeDate) + ), + } : {}), + }} + onClose={handleClose} + onInput={handleInput} + /> + {renderHiddenInput()} +
+ ); + }, +}); + +export { Type }; +export default Datepicker; diff --git a/client/src/themes/default/components/Datepicker/translations/en.yml b/client/src/themes/default/components/Datepicker/translations/en.yml new file mode 100644 index 000000000..501cea4ff --- /dev/null +++ b/client/src/themes/default/components/Datepicker/translations/en.yml @@ -0,0 +1,36 @@ +full-days: Full days? +full-day: Full day? + +select-hour: Select hour +select-hours: Select hours + +select-date: Select date +select-dates: Select dates + +predefined-period: + today: Today + yesterday: Yesterday + tomorrow: Tomorrow + this-week: This week + this-month: This month + this-year: This year + next-week: Next week + next-month: Next month + next-year: Next year + last-week: Last week + last-month: Last month + last-year: Last year + next-30days: Next 30 days + next-90days: Next 90 days + next-365days: Next 365 days + last-30days: Last 30 days + last-90days: Last 90 days + last-365days: Last 365 days + +range-format: + full: + start: 'D MMM, YYYY' + end: 'D MMM, YYYY' + same-year: + start: 'D MMM' + end: 'D MMM, YYYY' diff --git a/client/src/themes/default/components/Datepicker/translations/fr.yml b/client/src/themes/default/components/Datepicker/translations/fr.yml new file mode 100644 index 000000000..a96f5a55d --- /dev/null +++ b/client/src/themes/default/components/Datepicker/translations/fr.yml @@ -0,0 +1,36 @@ +full-days: Jours entiers? +full-day: Jour entier? + +select-hour: Sélectionner l'heure +select-hours: Sélectionner les heures + +select-date: Sélectionner la date +select-dates: Sélectionner les dates + +predefined-period: + today: Aujourd'hui + yesterday: Hier + tomorrow: Demain + this-week: Cette semaine + this-month: Ce mois + this-year: Cette année + next-week: Semaine prochaine + next-month: Mois prochain + next-year: Année prochaine + last-week: Semaine dernière + last-month: Mois dernier + last-year: Année dernière + next-30days: 30 prochains jours + next-90days: 90 prochains jours + next-365days: 365 prochains jours + last-30days: 30 derniers jours + last-90days: 90 derniers jours + last-365days: 365 derniers jours + +range-format: + full: + start: 'D MMM YYYY' + end: 'D MMM YYYY' + same-year: + start: 'D MMM' + end: 'D MMM YYYY' diff --git a/client/src/themes/default/components/DropZone/_variables.scss b/client/src/themes/default/components/DropZone/_variables.scss index c39b883d1..8d6e475fc 100644 --- a/client/src/themes/default/components/DropZone/_variables.scss +++ b/client/src/themes/default/components/DropZone/_variables.scss @@ -1,12 +1,41 @@ @use '~@/themes/default/style/globals'; -$drop-zone-background-color: transparent !default; -$drop-zone-color: globals.$text-base-color !default; -$drop-zone-border-radius: 10px !default; -$drop-zone-border-width: 5px !default; -$drop-zone-border-color: globals.$divider-color !default; -$drop-zone-icon-color: globals.$text-soft-color !default; - -$drop-zone-active-color: $drop-zone-color !default; -$drop-zone-active-icon-color: $drop-zone-icon-color !default; -$drop-zone-active-background-color: rgba(224, 68, 6, 0.3) !default; +/// Couleur de fond des zones de dépôt de fichier. +/// @type Color +$background-color: transparent !default; + +/// Couleur du texte des zones de dépôt de fichier. +/// @type Color +$color: globals.$text-base-color !default; + +/// Border radius des zones de dépôt de fichier. +/// @type Number +$border-radius: globals.$border-radius-large !default; + +/// Largeur de la bordure des zones de dépôt de fichier. +/// @type Number +$border-width: 5px !default; + +/// Couleur de la bordure des zones de dépôt de fichier. +/// @type Color +$border-color: globals.$divider-color !default; + +/// Couleur de l'icône des zones de dépôt de fichier. +/// @type Color +$icon-color: globals.$text-soft-color !default; + +// +// - État: Zone active. +// + +/// Couleur de fond des zones de dépôt de fichier lorsqu'elles sont actives. +/// @type Color +$active-background-color: rgba(224, 68, 6, 0.3) !default; + +/// Couleur du texte des zones de dépôt de fichier lorsqu'elles sont actives. +/// @type Color +$active-color: $color !default; + +/// Couleur de l'icône des zones de dépôt de fichier lorsqu'elles sont actives. +/// @type Color +$active-icon-color: $icon-color !default; diff --git a/client/src/themes/default/components/DropZone/index.scss b/client/src/themes/default/components/DropZone/index.scss index dd09d1f5a..b70cb4f58 100644 --- a/client/src/themes/default/components/DropZone/index.scss +++ b/client/src/themes/default/components/DropZone/index.scss @@ -11,16 +11,16 @@ justify-content: center; width: 100%; padding: 40px 20px; - border-radius: $drop-zone-border-radius; - background-color: $drop-zone-background-color; - color: $drop-zone-color; + border-radius: $border-radius; + background-color: $background-color; + color: $color; text-align: center; transition: background-color 300ms ease; cursor: pointer; user-select: none; /* stylelint-disable declaration-colon-newline-after, indentation */ - @if ($drop-zone-border-width > 0) { + @if ($border-width > 0) { background-image: url( 'data:image/svg+xml,' + '' + @@ -28,10 +28,10 @@ 'width="100%" ' + 'height="100%" ' + 'fill="none" ' + - 'rx="#{$drop-zone-border-radius}" ' + - 'ry="#{$drop-zone-border-radius}" ' + - 'stroke="#{globals.color-encode($drop-zone-border-color)}" ' + - 'stroke-width="#{$drop-zone-border-width}" ' + + 'rx="#{$border-radius}" ' + + 'ry="#{$border-radius}" ' + + 'stroke="#{globals.color-encode($border-color)}" ' + + 'stroke-width="#{$border-width}" ' + 'stroke-dasharray="6%2c 14" ' + 'stroke-dashoffset="0" ' + 'stroke-linecap="square" ' + @@ -47,7 +47,7 @@ &__icon { margin-bottom: 20px; - color: $drop-zone-icon-color; + color: $icon-color; font-size: 38px; } @@ -56,7 +56,7 @@ font-size: 1.2rem; &__sub-line { - color: color.adjust($drop-zone-color, $lightness: -10%); + color: color.adjust($color, $lightness: -10%); font-size: 1rem; font-style: italic; } @@ -76,15 +76,15 @@ &:active, &--dragging { - background-color: $drop-zone-active-background-color; - color: $drop-zone-active-color; + background-color: $active-background-color; + color: $active-color; #{$block}__icon { - color: $drop-zone-active-icon-color; + color: $active-icon-color; } #{$block}__instruction__sub-line { - color: color.adjust($drop-zone-active-color, $lightness: -10%); + color: color.adjust($active-color, $lightness: -10%); } } @@ -99,10 +99,10 @@ @keyframes FileManagerDropZone--gradient { 0%, 100% { - background: $drop-zone-background-color; + background: $background-color; } 50% { - background: $drop-zone-active-background-color; + background: $active-background-color; } } diff --git a/client/src/themes/default/components/DropZone/index.tsx b/client/src/themes/default/components/DropZone/index.tsx index 33a9ee4f4..dcee09e29 100644 --- a/client/src/themes/default/components/DropZone/index.tsx +++ b/client/src/themes/default/components/DropZone/index.tsx @@ -108,7 +108,8 @@ const DropZone = defineComponent({ handleClickOpenFileBrowser(e: Event) { e.stopPropagation(); - this.$refs.inputFileRef.click(); + const $inputFile = this.$refs.inputFile as HTMLInputElement | undefined; + $inputFile?.click(); }, handleAddFiles(e: Event) { @@ -179,7 +180,7 @@ const DropZone = defineComponent({
- ); }, -}; +}); + +export default EventBeneficiaries; diff --git a/client/src/themes/default/components/EventTechnicians/index.js b/client/src/themes/default/components/EventTechnicians/index.js index 4131e036f..77ad07f9b 100644 --- a/client/src/themes/default/components/EventTechnicians/index.js +++ b/client/src/themes/default/components/EventTechnicians/index.js @@ -1,9 +1,10 @@ import './index.scss'; -import EventTechnicianItem from './Item'; +import { defineComponent } from '@vue/composition-api'; import Icon from '@/themes/default/components/Icon'; +import Item from './Item'; // @vue/component -export default { +const EventTechnicians = defineComponent({ name: 'EventTechnicians', props: { eventTechnicians: { type: Array, required: true }, @@ -37,7 +38,7 @@ export default { {uniqueTechnicians.map(({ id, technician }) => ( - + ))} @@ -46,4 +47,6 @@ export default {
); }, -}; +}); + +export default EventTechnicians; diff --git a/client/src/themes/default/components/EventTotals/index.tsx b/client/src/themes/default/components/EventTotals/index.tsx index 6c58bd04f..a6c96ad7c 100644 --- a/client/src/themes/default/components/EventTotals/index.tsx +++ b/client/src/themes/default/components/EventTotals/index.tsx @@ -1,11 +1,10 @@ import './index.scss'; import { defineComponent } from '@vue/composition-api'; import formatAmount from '@/utils/formatAmount'; -import getEventMaterialItemsCount from '@/utils/getEventMaterialItemsCount'; import Fragment from '@/components/Fragment'; import type { PropType } from '@vue/composition-api'; -import type { Event } from '@/stores/api/events'; +import type { Event, EventMaterial } from '@/stores/api/events'; type Props = { /** L'événement dont on veut afficher les totaux. */ @@ -23,7 +22,14 @@ const EventTotals = defineComponent({ }, computed: { itemsCount(): number { - return getEventMaterialItemsCount(this.event.materials); + const { materials } = this.event; + + return materials.reduce( + (total: number, material: EventMaterial) => ( + total + material.pivot.quantity + ), + 0, + ); }, useTaxes(): boolean { @@ -82,7 +88,7 @@ const EventTotals = defineComponent({ {__('total')} {__('items-count', { count: itemsCount }, itemsCount)}
- {__('duration-days', { duration }, duration)} + {__('duration-days', { duration: duration.days }, duration.days)}
{__('total-replacement')} {formatAmount(totalReplacement, currency)} @@ -125,7 +131,7 @@ const EventTotals = defineComponent({
- {__('days-count', { duration }, duration)}, {__('ratio')} + {__('days-count', { duration: duration.days }, duration.days)}, {__('ratio')}
× {degressiveRate.toString()} diff --git a/client/src/themes/default/components/Fieldset/index.js b/client/src/themes/default/components/Fieldset/index.js index 26a2aa6ee..6991bff3d 100644 --- a/client/src/themes/default/components/Fieldset/index.js +++ b/client/src/themes/default/components/Fieldset/index.js @@ -4,19 +4,16 @@ import { defineComponent } from '@vue/composition-api'; // @vue/component const Fieldset = defineComponent({ name: 'Fieldset', - inject: { - verticalForm: { default: false }, - }, props: { title: { type: String, default: undefined }, help: { type: String, default: undefined }, }, render() { - const { title, help, verticalForm } = this; + const { title, help } = this; const children = this.$slots.default; const classNames = ['Fieldset', { - 'Fieldset--in-vertical-form': verticalForm, + 'Fieldset--with-help': !!help, }]; return ( diff --git a/client/src/themes/default/components/Fieldset/index.scss b/client/src/themes/default/components/Fieldset/index.scss index 9e69b4aef..c540fff91 100644 --- a/client/src/themes/default/components/Fieldset/index.scss +++ b/client/src/themes/default/components/Fieldset/index.scss @@ -9,27 +9,22 @@ } &__help { + margin: 0 0 globals.$spacing-medium; color: globals.$text-light-color; - - @include globals.icon('info-circle') { - margin-right: 0.3rem; - } + white-space: pre-line; } & + & { - margin-top: 40px; + margin-top: globals.$spacing-large; } // - // - Responsive + // - Variantes // - @media screen and (min-width: globals.$screen-tablet) { - &:not(&--in-vertical-form) { - #{$block}__title { - margin-left: globals.$form-label-width; - font-size: 1.1rem; - } + &--with-help { + #{$block}__title { + margin-bottom: globals.$spacing-small; } } } diff --git a/client/src/themes/default/components/FileManager/components/Document/index.scss b/client/src/themes/default/components/FileManager/components/Document/index.scss index 5c9bfa432..5247cf9a8 100644 --- a/client/src/themes/default/components/FileManager/components/Document/index.scss +++ b/client/src/themes/default/components/FileManager/components/Document/index.scss @@ -6,7 +6,7 @@ align-items: center; margin: 0 0 10px; padding: globals.$spacing-medium; - border-radius: globals.$input-border-radius; + border-radius: globals.$border-radius-normal; background: globals.$bg-color-emphasis; color: globals.$text-base-color; list-style: none; diff --git a/client/src/themes/default/components/FileManager/components/UploadArea/Upload/index.scss b/client/src/themes/default/components/FileManager/components/UploadArea/Upload/index.scss index 06d61ed89..f9bb198f8 100644 --- a/client/src/themes/default/components/FileManager/components/UploadArea/Upload/index.scss +++ b/client/src/themes/default/components/FileManager/components/UploadArea/Upload/index.scss @@ -6,7 +6,7 @@ position: relative; padding: globals.$spacing-medium globals.$spacing-large; - border-radius: 10px; + border-radius: globals.$border-radius-large; background: globals.$bg-color-emphasis; color: globals.$text-base-color; diff --git a/client/src/themes/default/components/FileManager/components/UploadArea/index.tsx b/client/src/themes/default/components/FileManager/components/UploadArea/index.tsx index 61a8af700..af2d16e2c 100644 --- a/client/src/themes/default/components/FileManager/components/UploadArea/index.tsx +++ b/client/src/themes/default/components/FileManager/components/UploadArea/index.tsx @@ -33,7 +33,10 @@ type Data = { uploads: Upload[], }; -/** Nombre d'upload simultanés maximum (au delà, les uploads seront placés dans une queue). */ +/** + * Nombre d'upload simultanés maximum (au delà, les + * uploads seront placés dans une queue). + */ const MAX_CONCURRENT_UPLOADS = 5; // @vue/component diff --git a/client/src/themes/default/components/FileManager/index.tsx b/client/src/themes/default/components/FileManager/index.tsx index f25943bb6..460ef360b 100644 --- a/client/src/themes/default/components/FileManager/index.tsx +++ b/client/src/themes/default/components/FileManager/index.tsx @@ -83,7 +83,6 @@ const FileManager = defineComponent({ * @returns `true` si un fichier est en cours d'upload, `false` sinon. */ isUploading(): boolean { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const $uploadAreaRef = this.$refs.uploadArea as ComponentRef; return !!$uploadAreaRef?.isUploading(); }, diff --git a/client/src/themes/default/components/FormField/index.js b/client/src/themes/default/components/FormField/index.js index c2c1dec93..f2969c897 100644 --- a/client/src/themes/default/components/FormField/index.js +++ b/client/src/themes/default/components/FormField/index.js @@ -2,7 +2,8 @@ import './index.scss'; import warning from 'warning'; import { computed, defineComponent } from '@vue/composition-api'; import Select from '@/themes/default/components/Select'; -import Datepicker, { TYPES as DATEPICKER_TYPES } from '@/themes/default/components/Datepicker'; +import Radio from '@/themes/default/components/Radio'; +import Datepicker, { Type as DatepickerType } from '@/themes/default/components/Datepicker'; import SwitchToggle from '@/themes/default/components/SwitchToggle'; import Input, { TYPES as INPUT_TYPES } from '@/themes/default/components/Input'; import Textarea from '@/themes/default/components/Textarea'; @@ -11,12 +12,13 @@ import InputColor from '@/themes/default/components/InputColor'; import Color from '@/utils/color'; const TYPES = [ - ...DATEPICKER_TYPES, + ...Object.values(DatepickerType), ...INPUT_TYPES, 'color', 'copy', 'static', 'select', + 'radio', 'textarea', 'switch', 'custom', @@ -59,7 +61,14 @@ export default defineComponent({ max: { type: Number, default: undefined }, addon: { type: String, default: undefined }, options: { type: Array, default: undefined }, - datepickerOptions: { type: Object, default: undefined }, + + // - Props. spécifiques aux date picker. + range: { type: Boolean, default: false }, + minDate: { type: [String, Object, Date, Number], default: undefined }, + maxDate: { type: [String, Object, Date, Number], default: undefined }, + disabledDate: { type: Function, default: undefined }, + withFullDaysToggle: { type: Boolean, default: false }, + withoutMinutes: { type: Boolean, default: false }, }, computed: { invalid() { @@ -81,24 +90,35 @@ export default defineComponent({ }, }, methods: { - handleChange(newValue) { - this.$emit('change', newValue); + // ------------------------------------------------------ + // - + // - Handlers + // - + // ------------------------------------------------------ + + handleChange(...newValue) { + this.$emit('change', ...newValue); }, - handleInput(newValue) { - this.$emit('input', newValue); + handleInput(...newValue) { + this.$emit('input', ...newValue); }, // ------------------------------------------------------ // - - // - Méthodes utilisables sur l'instance du composant + // - API Publique // - // ------------------------------------------------------ + /** + * Permet de donner le focus au champ de formulaire. + */ focus() { - if (this.$refs.inputRef) { - this.$refs.inputRef.focus(); - } + // FIXME: Devrait prendre en charge le focus de n'importe + // quel champ, pas seulement `` / `