Compare commits
139 Commits
feat/ajout
...
v0.0.73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bc484574f | ||
| ea1e3b074c | |||
|
|
4944611088 | ||
| fbfc7acfe4 | |||
|
|
92f54f600f | ||
| a905c6a1de | |||
|
|
995e7de2cc | ||
| 2408ccab67 | |||
| 82af4d4c1e | |||
|
|
11491b02c5 | ||
| 024af5887e | |||
|
|
91c0125876 | ||
| b510cdcc42 | |||
|
|
d0213c3212 | ||
| 3ac676689d | |||
|
|
9f47e81efd | ||
| 257b93e691 | |||
|
|
dc5320b324 | ||
| 09a641e5cf | |||
|
|
a0557b077b | ||
| 2d2b38eae4 | |||
|
|
d3581b8ce6 | ||
| 9e53be8ac3 | |||
|
|
2aafa2082a | ||
| 2b64f024b6 | |||
|
|
47cac04257 | ||
| 59d76c5f14 | |||
|
|
c48cc477da | ||
| 5967665e9f | |||
|
|
393c420983 | ||
| 456623b403 | |||
|
|
e2a8e89e55 | ||
| 92a5c48e5e | |||
|
|
6766985713 | ||
| c0d05264df | |||
|
|
9505201499 | ||
| 624591c096 | |||
|
|
e31bdce713 | ||
| 5d72beaf8d | |||
| 43f34015c6 | |||
|
|
ac5ce07e61 | ||
| e9fb36cc24 | |||
|
|
06a41c5f85 | ||
| f263a11fe8 | |||
|
|
c52f22472d | ||
| e7421e985e | |||
|
|
0d258ae9c6 | ||
| 7dd615ea34 | |||
|
|
6eee0745a7 | ||
| 845f94db8c | |||
|
|
86c0e74074 | ||
| be29daf4d1 | |||
|
|
08e7c1508c | ||
| 358da6a8ad | |||
|
|
67428186f6 | ||
| 09d108a1d5 | |||
|
|
f58dc36a0d | ||
| 15c0f414af | |||
|
|
9ed0ba702e | ||
| 93edd0a563 | |||
|
|
c361ef9bb9 | ||
| 7f3d9ef9c6 | |||
|
|
22b959de85 | ||
| d3bc2e11f1 | |||
|
|
d8b16f5e15 | ||
| 43213bc6d6 | |||
|
|
09666d9319 | ||
| 05ea33735d | |||
|
|
89c67f7e97 | ||
| d527e94bac | |||
|
|
579bdba65b | ||
| b1c3952d09 | |||
|
|
ab6de16319 | ||
| 800ab1d432 | |||
|
|
fade51d3ee | ||
| 9ca0a7511b | |||
|
|
d3dfde7060 | ||
| 90c2cfc665 | |||
|
|
9fc3e2f9bc | ||
| 329bb4cee5 | |||
|
|
d3af654858 | ||
| 168d8c78eb | |||
|
|
338d903cef | ||
| 42ce1e2d08 | |||
|
|
0d0aa788db | ||
| c010bdc262 | |||
|
|
0e905bfcbe | ||
| e6bb4ddf6a | |||
| 299ea84e87 | |||
| bb0b0092da | |||
| 33d21f6ae6 | |||
| 98ee62294d | |||
| 820386b87b | |||
| c17f7aa08a | |||
| 4a0d38d307 | |||
| e9948d6ac3 | |||
| 80d87b7c9b | |||
| a69556c554 | |||
| 13e8698673 | |||
| a34bdbfe8d | |||
| d8e1cdc72c | |||
| 2c54a8c950 | |||
| 7c85d91c78 | |||
| 149bced1c5 | |||
| 086279f962 | |||
| 1ce6357c1d | |||
| 9ae073e69e | |||
| 2cd05a39ba | |||
| 66e9c52914 | |||
| 80d6d72e37 | |||
| b883546575 | |||
| 038596951d | |||
| ac5a3493e7 | |||
| 7dc4fdd1c0 | |||
| 5395dfefda | |||
| c4f4107512 | |||
| 22f26ddb38 | |||
| d3289c8497 | |||
| 9f589bc86c | |||
| 66274e239b | |||
| f93a867dd4 | |||
| 744d8a4088 | |||
| cf693c0304 | |||
| 44bff2a4e5 | |||
| 6c1f14ae4d | |||
| 6f2218d2c9 | |||
| 9596d94617 | |||
| f99e5cd386 | |||
| 8f5730c3f6 | |||
| 42fafc5d39 | |||
| cc83242883 | |||
| 2d3ce2ca43 | |||
| 94ea49587a | |||
| 4a77449a41 | |||
| 9fb0fc12b8 | |||
| 14960d5e87 | |||
| ecb6f25159 | |||
| 566e7f132a | |||
| a2d20dafb1 |
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"WebFetch(domain:geo.api.gouv.fr)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
53
.env
53
.env
@@ -1,41 +1,22 @@
|
|||||||
# In all environments, the following files are loaded if they exist,
|
APP_ENV=
|
||||||
# the latter taking precedence over the former:
|
APP_DEBUG=
|
||||||
#
|
|
||||||
# * .env contains default values for the environment variables needed by the app
|
|
||||||
# * .env.local uncommitted file with local overrides
|
|
||||||
# * .env.$APP_ENV committed environment-specific defaults
|
|
||||||
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
|
||||||
#
|
|
||||||
# Real environment variables win over .env files.
|
|
||||||
#
|
|
||||||
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
|
||||||
# https://symfony.com/doc/current/configuration/secrets.html
|
|
||||||
#
|
|
||||||
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
|
||||||
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
|
||||||
APP_ENV=dev
|
|
||||||
APP_SECRET=
|
APP_SECRET=
|
||||||
APP_SHARE_DIR=var/share
|
|
||||||
###< symfony/framework-bundle ###
|
|
||||||
|
|
||||||
###> symfony/routing ###
|
DEFAULT_URI=
|
||||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
|
||||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
|
||||||
DEFAULT_URI=http://localhost
|
|
||||||
###< symfony/routing ###
|
|
||||||
|
|
||||||
###> doctrine/doctrine-bundle ###
|
|
||||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
|
||||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
|
||||||
#
|
|
||||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
|
||||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
|
||||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
|
||||||
#DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
|
||||||
###< doctrine/doctrine-bundle ###
|
|
||||||
|
|
||||||
###> nelmio/cors-bundle ###
|
###> nelmio/cors-bundle ###
|
||||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
CORS_ALLOW_ORIGIN=
|
||||||
###< nelmio/cors-bundle ###
|
###< nelmio/cors-bundle ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
JWT_SECRET_KEY=
|
||||||
|
JWT_PUBLIC_KEY=
|
||||||
|
JWT_PASSPHRASE=
|
||||||
|
COOKIE_SECURE=
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
|
|
||||||
|
# ADAPTER avec la vraie BDD (pas de variables docker)
|
||||||
|
DATABASE_URL=
|
||||||
|
|
||||||
|
PONT_BASCULE_BYPASS=
|
||||||
|
PONT_BASCULE_URL=
|
||||||
|
|||||||
23
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
23
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
name: "Merge Request"
|
||||||
|
about: "Template de MR"
|
||||||
|
title: "[#NUMERO_TICKET] TITRE TICKET"
|
||||||
|
ref: "main"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Numéro du ticket | Titre du ticket |
|
||||||
|
|------------------|-----------------|
|
||||||
|
| | |
|
||||||
|
|
||||||
|
## Description de la PR
|
||||||
|
|
||||||
|
## Modification du .env
|
||||||
|
|
||||||
|
## Check list
|
||||||
|
|
||||||
|
- [ ] Pas de régression
|
||||||
|
- [ ] TU/TI/TF rédigée
|
||||||
|
- [ ] TU/TI/TF OK
|
||||||
|
- [ ] CHANGELOG modifié
|
||||||
65
.gitea/workflows/auto-tag-develop.yml
Normal file
65
.gitea/workflows/auto-tag-develop.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Auto Tag Develop
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tag:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Create next tag from config/version.yaml
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Skip if current commit already has a vX.Y.Z tag
|
||||||
|
if git tag --points-at HEAD | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||||
|
echo "Tag already exists on this commit. Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
changed_version=false
|
||||||
|
if git diff --name-only "${{ gitea.event.before }}" "${{ gitea.event.after }}" | grep -q '^config/version\.yaml$'; then
|
||||||
|
changed_version=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
read_version() {
|
||||||
|
awk -F': *' '/app\.version:/{print $2}' config/version.yaml | tr -d '[:space:]' | tr -d "'\""
|
||||||
|
}
|
||||||
|
|
||||||
|
if $changed_version; then
|
||||||
|
version="$(read_version)"
|
||||||
|
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "Invalid version in version.yaml: $version" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
last_tag="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)"
|
||||||
|
if [ -z "$last_tag" ]; then
|
||||||
|
version="0.1.0"
|
||||||
|
else
|
||||||
|
base="${last_tag#v}"
|
||||||
|
IFS='.' read -r major minor patch <<< "$base"
|
||||||
|
version="${major}.${minor}.$((patch + 1))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "parameters:\\n app.version: '%s'\\n" "$version" > config/version.yaml
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "gitea-actions@local"
|
||||||
|
git add config/version.yaml
|
||||||
|
git commit -m "chore: bump version to v$version" || true
|
||||||
|
git push origin develop || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
tag="v$version"
|
||||||
|
git tag "$tag"
|
||||||
|
git push origin "$tag"
|
||||||
65
.gitea/workflows/release-artefact.yml
Normal file
65
.gitea/workflows/release-artefact.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Build Release Artefact
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v0.0.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: "8.4"
|
||||||
|
extensions: mbstring, intl, pdo_pgsql, xml, curl, zip, gd
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
|
- name: Install backend deps (prod)
|
||||||
|
env:
|
||||||
|
APP_ENV: prod
|
||||||
|
APP_DEBUG: "0"
|
||||||
|
run: composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
|
||||||
|
|
||||||
|
- name: Build frontend (static)
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ NUXT_PUBLIC_GEO_API_BASE=https://geo.api.gouv.fr npm run generate
|
||||||
|
test -f .output/public/index.html
|
||||||
|
|
||||||
|
- name: Build artefact
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p release
|
||||||
|
tar -czf "release/ferme-${GITHUB_REF_NAME}.tar.gz" \
|
||||||
|
bin \
|
||||||
|
config \
|
||||||
|
migrations \
|
||||||
|
public \
|
||||||
|
src \
|
||||||
|
templates \
|
||||||
|
vendor \
|
||||||
|
composer.json \
|
||||||
|
composer.lock \
|
||||||
|
symfony.lock \
|
||||||
|
frontend/.output
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: release/ferme-${{ github.ref_name }}.tar.gz
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
|
/.env.prod
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
/.env.*.local
|
/.env.*.local
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
/var/
|
/var/
|
||||||
/vendor/
|
/vendor/
|
||||||
/LOG/
|
/LOG/
|
||||||
|
/config/jwt/*.pem
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
###> friendsofphp/php-cs-fixer ###
|
###> friendsofphp/php-cs-fixer ###
|
||||||
@@ -23,3 +25,7 @@
|
|||||||
###> docker ###
|
###> docker ###
|
||||||
docker/.env.docker.local
|
docker/.env.docker.local
|
||||||
###< docker ###
|
###< docker ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
/config/jwt/*.pem
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
|
|||||||
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AskMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/dataSources.xml
generated
12
.idea/dataSources.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
|
||||||
<data-source source="LOCAL" name="ferme" uuid="f407a514-c6b4-4b26-9555-445a85892502">
|
|
||||||
<driver-ref>postgresql</driver-ref>
|
|
||||||
<synchronize>true</synchronize>
|
|
||||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
|
||||||
<jdbc-url>jdbc:postgresql://localhost:5432/ferme</jdbc-url>
|
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
|
||||||
</data-source>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/db-forest-config.xml
generated
Normal file
6
.idea/db-forest-config.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="db-tree-configuration">
|
||||||
|
<option name="data" value="---------------------------------------- 1:0:f407a514-c6b4-4b26-9555-445a85892502 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc 3:0:9cad43df-2147-4989-b7a4-443067034884 4:0:09e221b8-067a-488b-9c1d-4e155a333079 " />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
.idea/ferme.iml
generated
19
.idea/ferme.iml
generated
@@ -137,6 +137,25 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/dompdf" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/php-font-lib" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/php-svg-lib" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/sabberworm/php-css-parser" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/thecodingmachine/safe" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/lcobucci/jwt" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/malio/ednotif-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bridge" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/data-fixtures" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
|
||||||
|
<excludePattern pattern="reference.php" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
285
.idea/php.xml
generated
285
.idea/php.xml
generated
@@ -4,148 +4,184 @@
|
|||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PHPCSFixerOptionsConfiguration">
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="codingStandard" value="Custom" />
|
||||||
|
<option name="rulesetPath" value="$PROJECT_DIR$/.php-cs-fixer.dist.php" />
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PHPCodeSnifferOptionsConfiguration">
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
<option name="highlightLevel" value="WARNING" />
|
<option name="highlightLevel" value="WARNING" />
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="PhpCSFixer">
|
||||||
|
<phpcsfixer_settings>
|
||||||
|
<PhpCSFixerConfiguration tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
|
||||||
|
<phpcs_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="990ff521-e6e9-4080-9cc9-228367d597f9" tool_path="\\wsl.localhost\Ubuntu-24.04\home\matte\Ferme\vendor\bin\php-cs-fixer" timeout="30000" />
|
||||||
|
</phpcsfixer_settings>
|
||||||
|
</component>
|
||||||
|
<component name="PhpCodeSniffer">
|
||||||
|
<phpcs_settings>
|
||||||
|
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="8475dcce-5d1d-4a2c-9e2f-7454868f1931" timeout="30000" />
|
||||||
|
</phpcs_settings>
|
||||||
|
</component>
|
||||||
<component name="PhpIncludePathManager">
|
<component name="PhpIncludePathManager">
|
||||||
<include_path>
|
<include_path>
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/child-process" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/nelmio/cors-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/common" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/serializer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/child-process" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/nelmio/cors-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/common" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/serializer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/malio/ednotif-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||||
|
<component name="PhpStan">
|
||||||
|
<PhpStan_settings>
|
||||||
|
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="8475dcce-5d1d-4a2c-9e2f-7454868f1931" timeout="60000" />
|
||||||
|
</PhpStan_settings>
|
||||||
|
</component>
|
||||||
<component name="PhpStanOptionsConfiguration">
|
<component name="PhpStanOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
@@ -154,6 +190,11 @@
|
|||||||
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
|
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
|
||||||
</phpunit_settings>
|
</phpunit_settings>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="Psalm">
|
||||||
|
<Psalm_settings>
|
||||||
|
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="8475dcce-5d1d-4a2c-9e2f-7454868f1931" timeout="60000" />
|
||||||
|
</Psalm_settings>
|
||||||
|
</component>
|
||||||
<component name="PsalmOptionsConfiguration">
|
<component name="PsalmOptionsConfiguration">
|
||||||
<option name="transferred" value="true" />
|
<option name="transferred" value="true" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
823
.idea/workspace.xml
generated
823
.idea/workspace.xml
generated
@@ -1,6 +1,827 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ComposerSettings">
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de l'api de l'état pour chercher les villes via le CP">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/scripts/deploy-release.sh" beforeDir="false" afterPath="$PROJECT_DIR$/scripts/deploy-release.sh" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="ComposerSettings" synchronizationState="SYNCHRONIZE">
|
||||||
|
<pharConfigPath>$PROJECT_DIR$/composer.json</pharConfigPath>
|
||||||
<execution />
|
<execution />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="CopilotPersistence">
|
||||||
|
<persistenceIdMap>
|
||||||
|
<entry key="_//wsl.localhost/Ubuntu-24.04/home/kevin/Stage/Ferme" value="381AhnCm9yPeOiWgMObKHhtgv2C" />
|
||||||
|
</persistenceIdMap>
|
||||||
|
</component>
|
||||||
|
<component name="EmbeddingIndexingInfo">
|
||||||
|
<option name="cachedIndexableFilesCount" value="151" />
|
||||||
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="TypeScript File" />
|
||||||
|
<option value="PHP File" />
|
||||||
|
<option value="Vue Composition API Component" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
|
<map>
|
||||||
|
<entry key="$PROJECT_DIR$" value="feat/327-voir-modifier-une-expedition-terminee" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
</component>
|
||||||
|
<component name="McpProjectServerCommands">
|
||||||
|
<commands />
|
||||||
|
<urls />
|
||||||
|
</component>
|
||||||
|
<component name="PhpServers">
|
||||||
|
<servers>
|
||||||
|
<server host="localhost" id="36c0c232-9151-4654-a36c-e0f5fd99da91" name="ferme-docker" port="8080" use_path_mappings="true">
|
||||||
|
<path_mappings>
|
||||||
|
<mapping local-root="$PROJECT_DIR$" remote-root="/var/www/html" />
|
||||||
|
</path_mappings>
|
||||||
|
</server>
|
||||||
|
</servers>
|
||||||
|
</component>
|
||||||
|
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="C:/php-8.4.3/php.exe">
|
||||||
|
<include_path>
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/child-process" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/nelmio/cors-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/common" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/serializer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/malio/ednotif-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||||
|
</include_path>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"customColor": "",
|
||||||
|
"associatedIndex": 5
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="381AhnCm9yPeOiWgMObKHhtgv2C" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="autoscrollFromSource" value="true" />
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
|
"git-widget-placeholder": "develop",
|
||||||
|
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/m-tristan/workspace/Ferme",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"settings.editor.selected.configurable": "advanced.settings",
|
||||||
|
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
},
|
||||||
|
"keyToStringList": {
|
||||||
|
"DatabaseDriversLRU": [
|
||||||
|
"postgresql"
|
||||||
|
],
|
||||||
|
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||||
|
"TEXT"
|
||||||
|
],
|
||||||
|
"vue.recent.templates": [
|
||||||
|
"Vue Composition API Component"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||||
|
<recent name="$PROJECT_DIR$/frontend/components/commun" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\pages\shipment" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\composables" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\components\shipment" />
|
||||||
|
</key>
|
||||||
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="C:\Users\m-tristan\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||||
|
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||||
|
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.31033.138" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="" />
|
||||||
|
<created>1767956826164</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1767956826164</updated>
|
||||||
|
<workItem from="1767956827666" duration="7866000" />
|
||||||
|
<workItem from="1768201706520" duration="13383000" />
|
||||||
|
<workItem from="1768287908317" duration="28058000" />
|
||||||
|
<workItem from="1768374298711" duration="12403000" />
|
||||||
|
<workItem from="1768460547451" duration="26946000" />
|
||||||
|
<workItem from="1768547023783" duration="11371000" />
|
||||||
|
<workItem from="1768894030675" duration="83922000" />
|
||||||
|
<workItem from="1769413136483" duration="58000" />
|
||||||
|
<workItem from="1769413279223" duration="40490000" />
|
||||||
|
<workItem from="1769612160652" duration="23952000" />
|
||||||
|
<workItem from="1769696465294" duration="8573000" />
|
||||||
|
<workItem from="1769756623432" duration="21592000" />
|
||||||
|
<workItem from="1770015653091" duration="73000" />
|
||||||
|
<workItem from="1770040138216" duration="6492000" />
|
||||||
|
<workItem from="1770050834470" duration="1873000" />
|
||||||
|
<workItem from="1770054381680" duration="1292000" />
|
||||||
|
<workItem from="1770055690365" duration="370000" />
|
||||||
|
<workItem from="1770056515646" duration="21000" />
|
||||||
|
<workItem from="1770102495553" duration="2280000" />
|
||||||
|
<workItem from="1770195604082" duration="90000" />
|
||||||
|
<workItem from="1770195718952" duration="215000" />
|
||||||
|
<workItem from="1770195959162" duration="18915000" />
|
||||||
|
<workItem from="1770274844804" duration="3940000" />
|
||||||
|
<workItem from="1770798536017" duration="20774000" />
|
||||||
|
<workItem from="1770879701502" duration="25805000" />
|
||||||
|
<workItem from="1770966186589" duration="914000" />
|
||||||
|
<workItem from="1770967274060" duration="2388000" />
|
||||||
|
<workItem from="1772466451823" duration="598000" />
|
||||||
|
<workItem from="1772626984813" duration="969000" />
|
||||||
|
<workItem from="1772786360430" duration="21000" />
|
||||||
|
<workItem from="1772786475316" duration="3016000" />
|
||||||
|
<workItem from="1773049125640" duration="406000" />
|
||||||
|
<workItem from="1773049540928" duration="539000" />
|
||||||
|
<workItem from="1773050154207" duration="1879000" />
|
||||||
|
<workItem from="1773212999001" duration="652000" />
|
||||||
|
<workItem from="1773215356754" duration="5754000" />
|
||||||
|
<workItem from="1773756072697" duration="5450000" />
|
||||||
|
<workItem from="1773766075191" duration="6202000" />
|
||||||
|
<workItem from="1773824491213" duration="16281000" />
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00026" summary="fix : doc de déploiement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769094376813</created>
|
||||||
|
<option name="number" value="00026" />
|
||||||
|
<option name="presentableId" value="LOCAL-00026" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769094376813</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00027" summary="fix : doc et script de déploiement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769096187792</created>
|
||||||
|
<option name="number" value="00027" />
|
||||||
|
<option name="presentableId" value="LOCAL-00027" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769096187792</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00028" summary="fix : doc et script de déploiement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769097091268</created>
|
||||||
|
<option name="number" value="00028" />
|
||||||
|
<option name="presentableId" value="LOCAL-00028" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769097091268</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00029" summary="fix : gitea workflow">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769097476629</created>
|
||||||
|
<option name="number" value="00029" />
|
||||||
|
<option name="presentableId" value="LOCAL-00029" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769097476629</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00030" summary="fix : script de déploiement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769098182184</created>
|
||||||
|
<option name="number" value="00030" />
|
||||||
|
<option name="presentableId" value="LOCAL-00030" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769098182184</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00031" summary="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769098861988</created>
|
||||||
|
<option name="number" value="00031" />
|
||||||
|
<option name="presentableId" value="LOCAL-00031" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769098861988</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00032" summary="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769100048933</created>
|
||||||
|
<option name="number" value="00032" />
|
||||||
|
<option name="presentableId" value="LOCAL-00032" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769100048933</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00033" summary="feat : ajout de la debug bar en mod dev">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769177611987</created>
|
||||||
|
<option name="number" value="00033" />
|
||||||
|
<option name="presentableId" value="LOCAL-00033" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769177611987</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00034" summary="feat : ajout du bundle Malio ednotif pour l'utilisation des WS">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769184861047</created>
|
||||||
|
<option name="number" value="00034" />
|
||||||
|
<option name="presentableId" value="LOCAL-00034" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769184861047</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00035" summary="fix : modification de la conf du bundle ednotif">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769434793487</created>
|
||||||
|
<option name="number" value="00035" />
|
||||||
|
<option name="presentableId" value="LOCAL-00035" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769434793487</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00036" summary="feat : update du CHANGELOG.md">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769435038236</created>
|
||||||
|
<option name="number" value="00036" />
|
||||||
|
<option name="presentableId" value="LOCAL-00036" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769435038236</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00037" summary="feat : finalisation de l'étape 1 "Réception" (formulaire)">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769529522614</created>
|
||||||
|
<option name="number" value="00037" />
|
||||||
|
<option name="presentableId" value="LOCAL-00037" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769529522614</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00038" summary="feat : ajout du numéro identification des receptions et ajustement du bon de reception">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769676223697</created>
|
||||||
|
<option name="number" value="00038" />
|
||||||
|
<option name="presentableId" value="LOCAL-00038" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769676223697</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00039" summary="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769700808988</created>
|
||||||
|
<option name="number" value="00039" />
|
||||||
|
<option name="presentableId" value="LOCAL-00039" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769700808988</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00040" summary="feat : mise en place de composant UI pour les select, checkbox, date, text">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769705141157</created>
|
||||||
|
<option name="number" value="00040" />
|
||||||
|
<option name="presentableId" value="LOCAL-00040" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769705141157</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00041" summary="feat : update CHANGELOG.md">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769705240487</created>
|
||||||
|
<option name="number" value="00041" />
|
||||||
|
<option name="presentableId" value="LOCAL-00041" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769705240487</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00042" summary="feat : ajout de commentaire">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769760766200</created>
|
||||||
|
<option name="number" value="00042" />
|
||||||
|
<option name="presentableId" value="LOCAL-00042" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769760766200</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00043" summary="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769768517942</created>
|
||||||
|
<option name="number" value="00043" />
|
||||||
|
<option name="presentableId" value="LOCAL-00043" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769768517943</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00044" summary="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769770092190</created>
|
||||||
|
<option name="number" value="00044" />
|
||||||
|
<option name="presentableId" value="LOCAL-00044" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769770092190</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00045" summary="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769770142624</created>
|
||||||
|
<option name="number" value="00045" />
|
||||||
|
<option name="presentableId" value="LOCAL-00045" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769770142624</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00046" summary="feat : mise à jour du bon de réception">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1769782099473</created>
|
||||||
|
<option name="number" value="00046" />
|
||||||
|
<option name="presentableId" value="LOCAL-00046" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1769782099473</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00047" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770131226364</created>
|
||||||
|
<option name="number" value="00047" />
|
||||||
|
<option name="presentableId" value="LOCAL-00047" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770131226364</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00048" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770206668867</created>
|
||||||
|
<option name="number" value="00048" />
|
||||||
|
<option name="presentableId" value="LOCAL-00048" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770206668867</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00049" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770217875423</created>
|
||||||
|
<option name="number" value="00049" />
|
||||||
|
<option name="presentableId" value="LOCAL-00049" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770217875423</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00050" summary="feat : creer une nouvelle expedtion (WIP)">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770736570645</created>
|
||||||
|
<option name="number" value="00050" />
|
||||||
|
<option name="presentableId" value="LOCAL-00050" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770736570645</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00051" summary="feat : ajout d'une page de creation d'une expedition">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770880791564</created>
|
||||||
|
<option name="number" value="00051" />
|
||||||
|
<option name="presentableId" value="LOCAL-00051" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770880791565</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00052" summary="feat : changelog">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770881437439</created>
|
||||||
|
<option name="number" value="00052" />
|
||||||
|
<option name="presentableId" value="LOCAL-00052" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770881437439</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00053" summary="feat : lister les expeditions terminees">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770883114609</created>
|
||||||
|
<option name="number" value="00053" />
|
||||||
|
<option name="presentableId" value="LOCAL-00053" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770883114609</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00054" summary="feat : lister les expeditions terminees">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770884154297</created>
|
||||||
|
<option name="number" value="00054" />
|
||||||
|
<option name="presentableId" value="LOCAL-00054" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770884154297</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00055" summary="fix : corrections diverses">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770969471135</created>
|
||||||
|
<option name="number" value="00055" />
|
||||||
|
<option name="presentableId" value="LOCAL-00055" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770969471135</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00056" summary="fix : corrections frontend">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772094268366</created>
|
||||||
|
<option name="number" value="00056" />
|
||||||
|
<option name="presentableId" value="LOCAL-00056" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772094268366</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00057" summary="feat : affichage et modification expédition et modification bouton valider">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772111964268</created>
|
||||||
|
<option name="number" value="00057" />
|
||||||
|
<option name="presentableId" value="LOCAL-00057" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772111964268</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00058" summary="fix : erreur customer adress et bouton valider oublie">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772112729501</created>
|
||||||
|
<option name="number" value="00058" />
|
||||||
|
<option name="presentableId" value="LOCAL-00058" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772112729502</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00059" summary="feat : changelog update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772112812677</created>
|
||||||
|
<option name="number" value="00059" />
|
||||||
|
<option name="presentableId" value="LOCAL-00059" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772112812677</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00060" summary="feat : changelog update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772177400063</created>
|
||||||
|
<option name="number" value="00060" />
|
||||||
|
<option name="presentableId" value="LOCAL-00060" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772177400063</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00061" summary="feat : changelog update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772177614438</created>
|
||||||
|
<option name="number" value="00061" />
|
||||||
|
<option name="presentableId" value="LOCAL-00061" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772177614438</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00062" summary="fix : color tab">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772178540489</created>
|
||||||
|
<option name="number" value="00062" />
|
||||||
|
<option name="presentableId" value="LOCAL-00062" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772178540489</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00063" summary="feat : modification front de la page admin transporteur">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180053740</created>
|
||||||
|
<option name="number" value="00063" />
|
||||||
|
<option name="presentableId" value="LOCAL-00063" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180053740</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00064" summary="fix : espacement et changelog">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180581178</created>
|
||||||
|
<option name="number" value="00064" />
|
||||||
|
<option name="presentableId" value="LOCAL-00064" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180581178</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00065" summary="fix : espacement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180684250</created>
|
||||||
|
<option name="number" value="00065" />
|
||||||
|
<option name="presentableId" value="LOCAL-00065" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180684250</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00066" summary="fix : espacement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180972984</created>
|
||||||
|
<option name="number" value="00066" />
|
||||||
|
<option name="presentableId" value="LOCAL-00066" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180972984</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00067" summary="fix : text">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772182545592</created>
|
||||||
|
<option name="number" value="00067" />
|
||||||
|
<option name="presentableId" value="LOCAL-00067" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772182545592</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00068" summary="feat : front page admin bovin et changelog">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772182707441</created>
|
||||||
|
<option name="number" value="00068" />
|
||||||
|
<option name="presentableId" value="LOCAL-00068" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772182707441</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00069" summary="fix : on ne bloque plus le poids max d'une pesée">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772447581744</created>
|
||||||
|
<option name="number" value="00069" />
|
||||||
|
<option name="presentableId" value="LOCAL-00069" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772447581744</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00070" summary="feat : ajout de supplier dans la feed et fixtures">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773761787472</created>
|
||||||
|
<option name="number" value="00070" />
|
||||||
|
<option name="presentableId" value="LOCAL-00070" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773761787472</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00071" summary="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773766207721</created>
|
||||||
|
<option name="number" value="00071" />
|
||||||
|
<option name="presentableId" value="LOCAL-00071" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773766207721</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00072" summary="fix : on ne pèse plus automatiquement + fix message de création réception">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773826699115</created>
|
||||||
|
<option name="number" value="00072" />
|
||||||
|
<option name="presentableId" value="LOCAL-00072" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773826699115</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00073" summary="fix : correction des retours de la V0">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773841634554</created>
|
||||||
|
<option name="number" value="00073" />
|
||||||
|
<option name="presentableId" value="LOCAL-00073" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773841634554</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00074" summary="feat : ajout de l'api de l'état pour chercher les villes via le CP">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773842791819</created>
|
||||||
|
<option name="number" value="00074" />
|
||||||
|
<option name="presentableId" value="LOCAL-00074" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773842791819</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="75" />
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<component name="Vcs.Log.Tabs.Properties">
|
||||||
|
<option name="RECENT_FILTERS">
|
||||||
|
<map>
|
||||||
|
<entry key="Branch">
|
||||||
|
<value>
|
||||||
|
<list>
|
||||||
|
<RecentGroup>
|
||||||
|
<option name="FILTER_VALUES">
|
||||||
|
<option value="HEAD" />
|
||||||
|
</option>
|
||||||
|
</RecentGroup>
|
||||||
|
<RecentGroup>
|
||||||
|
<option name="FILTER_VALUES">
|
||||||
|
<option value="develop" />
|
||||||
|
</option>
|
||||||
|
</RecentGroup>
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="TAB_STATES">
|
||||||
|
<map>
|
||||||
|
<entry key="MAIN">
|
||||||
|
<value>
|
||||||
|
<State>
|
||||||
|
<option name="FILTERS">
|
||||||
|
<map>
|
||||||
|
<entry key="branch">
|
||||||
|
<value>
|
||||||
|
<list>
|
||||||
|
<option value="HEAD" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</State>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="VcsManagerConfiguration">
|
||||||
|
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||||
|
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||||
|
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||||
|
<MESSAGE value="feat : creer une nouvelle expedtion (WIP)" />
|
||||||
|
<MESSAGE value="feat : ajout d'une page de creation d'une expedition" />
|
||||||
|
<MESSAGE value="feat : changelog" />
|
||||||
|
<MESSAGE value="feat : lister les expeditions terminees" />
|
||||||
|
<MESSAGE value="fix: corrections diverses" />
|
||||||
|
<MESSAGE value="fix : corrections diverses" />
|
||||||
|
<MESSAGE value="fix : corrections frontend" />
|
||||||
|
<MESSAGE value="feat : affichage et modification expédition et modification bouton valider" />
|
||||||
|
<MESSAGE value="fix : erreur customer adress et bouton valider oublie" />
|
||||||
|
<MESSAGE value="feat : changelog update" />
|
||||||
|
<MESSAGE value="fix : color tab" />
|
||||||
|
<MESSAGE value="feat : modification front de la page admin transporteur" />
|
||||||
|
<MESSAGE value="fix : espacement et changelog" />
|
||||||
|
<MESSAGE value="fix : espacement" />
|
||||||
|
<MESSAGE value="fix : text" />
|
||||||
|
<MESSAGE value="feat : front page admin bovin et changelog" />
|
||||||
|
<MESSAGE value="fix : on ne bloque plus le poids max d'une pesée" />
|
||||||
|
<MESSAGE value="feat : ajout de supplier dans la feed et fixtures" />
|
||||||
|
<MESSAGE value="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu" />
|
||||||
|
<MESSAGE value="fix : on ne pèse plus automatiquement + fix message de création réception" />
|
||||||
|
<MESSAGE value="fix : correction des retours de la V0" />
|
||||||
|
<MESSAGE value="feat : ajout de l'api de l'état pour chercher les villes via le CP" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout de l'api de l'état pour chercher les villes via le CP" />
|
||||||
|
</component>
|
||||||
|
<component name="XDebuggerManager">
|
||||||
|
<breakpoint-manager>
|
||||||
|
<breakpoints>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/Entity/ReceptionPelletBuilding.php</url>
|
||||||
|
<line>6</line>
|
||||||
|
<option name="timeStamp" value="3" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/Entity/Shipment.php</url>
|
||||||
|
<line>6</line>
|
||||||
|
<option name="timeStamp" value="45" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
|
<url>file://$PROJECT_DIR$/frontend/services/dto/shipment-data.ts</url>
|
||||||
|
<option name="timeStamp" value="43" />
|
||||||
|
</line-breakpoint>
|
||||||
|
</breakpoints>
|
||||||
|
</breakpoint-manager>
|
||||||
|
</component>
|
||||||
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
|
<expand />
|
||||||
|
<select />
|
||||||
|
</component>
|
||||||
|
<component name="github-copilot-workspace">
|
||||||
|
<instructionFileLocations>
|
||||||
|
<option value=".github/instructions" />
|
||||||
|
</instructionFileLocations>
|
||||||
|
<promptFileLocations>
|
||||||
|
<option value=".github/prompts" />
|
||||||
|
</promptFileLocations>
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
||||||
65
CHANGELOG.md
Normal file
65
CHANGELOG.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Liste des évolutions du projet Ferme
|
||||||
|
|
||||||
|
## [0.0.0]
|
||||||
|
### Parameters
|
||||||
|
Ajouter dans le fichier .env
|
||||||
|
- DEFAULT_URI
|
||||||
|
- DATABASE_URL
|
||||||
|
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||||
|
- PONT_BASCULE_URL
|
||||||
|
- JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
||||||
|
- JWT_PUBLIC_KEY
|
||||||
|
- JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||||
|
- COOKIE_SECURE=0 (en dev 0 et en prod 1)
|
||||||
|
- EDNOTIF_EXPLOITATION_CODE
|
||||||
|
- EDNOTIF_EXPLOITATION_NUMERO
|
||||||
|
- EDNOTIF_LOGIN
|
||||||
|
- EDNOTIF_PASSWORD
|
||||||
|
|
||||||
|
Ajouter dans le fichier .env du frontend
|
||||||
|
- NUXT_PUBLIC_API_BASE
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* [#203] Réceptions — Parcours de pesée multi-étapes (début)
|
||||||
|
* [#202] Authentification — Connexion utilisateur (JWT)
|
||||||
|
* Ajout du bundle malio/ednotif-bundle
|
||||||
|
* Ajout de composant UI
|
||||||
|
* Finalisation de la partie réception de marchandise
|
||||||
|
* [#267] Lister les réceptions en attente
|
||||||
|
* [#268] Lister les réceptions terminées
|
||||||
|
* [#316] Admin liste des transporteurs
|
||||||
|
* [#312] Creation administration listing fournisseurs
|
||||||
|
* [#315] Creation page admin utilisateur
|
||||||
|
* [#317] Admin modification creation transporteur
|
||||||
|
* [#318] Affichage modification reception terminée
|
||||||
|
* [#320] Affichage modification reception terminée suite
|
||||||
|
* [#271] Créer une nouvelle expédition (étape 1)
|
||||||
|
* [#272] Créer une nouvelle expédition (étape 2)
|
||||||
|
* [#273] Créer une nouvelle expédition (étape 3)
|
||||||
|
* [#256] Créer une nouvelle réception (étape 3 - bovin)
|
||||||
|
* [#314] Création d'une page d'administration : listing des utilisateurs
|
||||||
|
* [#313] Admin modification creation fournisseur
|
||||||
|
* [#275] Lister les expéditions en attente
|
||||||
|
* [#276] Lister les expéditions terminées
|
||||||
|
* [#324] Creation page admin listing clients
|
||||||
|
* [#326] Admin modification creation client
|
||||||
|
* [#325] Correction diverses
|
||||||
|
* fix layout admin
|
||||||
|
* Creation page admin listing bovins
|
||||||
|
* Creation page admin ajout/modification bovins
|
||||||
|
* [#331] Mettre à jour l'entité Shipment et bovin_shipment
|
||||||
|
* [#278] Plan du site
|
||||||
|
* [#334] Correctifs
|
||||||
|
* [#332] Refonte écran réception terminée
|
||||||
|
* [#327] afficher/modifier écran expédition terminée
|
||||||
|
* [#352] modification front admin fournisseur
|
||||||
|
* [#355] modification front admin transporteur
|
||||||
|
* [#356] front page admin bovin
|
||||||
|
* [#353] modification front admin client
|
||||||
|
* [#353] modification front admin utilisateur
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
160
CLAUDE.md
Normal file
160
CLAUDE.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Backend:** Symfony 8 + API Platform 4 (PHP 8.4)
|
||||||
|
- **Frontend:** Nuxt 4 (Vue 3, Pinia, Tailwind, Zod) in `frontend/`
|
||||||
|
- **Infra:** Docker (PHP-FPM + Nginx), Apache vhost serves API sous `/api` et frontend depuis `frontend/dist`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
make start # Démarrer les containers
|
||||||
|
make stop # Arrêter les containers
|
||||||
|
make restart # Redémarrer les containers
|
||||||
|
make shell # Shell dans le container PHP
|
||||||
|
|
||||||
|
# Install complet
|
||||||
|
make install # composer install + migrations + build frontend
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
make composer-install # Installer dépendances PHP
|
||||||
|
make migration-migrate # Lancer les migrations
|
||||||
|
make fixtures # Charger les fixtures
|
||||||
|
make cache-clear # Vider le cache Symfony
|
||||||
|
make test # Lancer les tests PHPUnit
|
||||||
|
make test FILES=tests/path/to/TestFile.php # Test spécifique
|
||||||
|
make php-cs-fixer-allow-risky FILES=src/... # Fixer le style
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
make build-nuxtJS # npm install + build:dist (dans le container)
|
||||||
|
make dev-nuxt # Serveur dev Nuxt (dans le container)
|
||||||
|
# Ou directement dans frontend/ :
|
||||||
|
cd frontend && npm run dev # Dev server (port 3000)
|
||||||
|
cd frontend && npm run build:dist # Build production
|
||||||
|
|
||||||
|
# Base de données
|
||||||
|
make db-reset # ⚠️ Supprime et recrée la BDD + migrations + fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture backend
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── ApiResource/ # Ressources API Platform custom
|
||||||
|
├── Command/ # Commandes Symfony (dont app:seed)
|
||||||
|
├── DataFixtures/ # Fixtures Doctrine
|
||||||
|
├── Dto/ # DTOs (ex: PontBasculeReading)
|
||||||
|
├── Entity/ # Entités Doctrine (= ressources API Platform)
|
||||||
|
├── Exception/ # Exceptions custom (PontBasculeException)
|
||||||
|
├── Kernel.php
|
||||||
|
├── Service/ # Services métier (PontBasculePayloadDecoder…)
|
||||||
|
└── State/ # State providers/processors API Platform
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture frontend
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── components/
|
||||||
|
│ ├── ui/ # Composants réutilisables, auto-importés avec préfixe Ui (ex: UiLoadingDots)
|
||||||
|
│ ├── reception/ # Composants métier réception
|
||||||
|
│ ├── shipment/ # Composants métier expédition
|
||||||
|
│ ├── workflow/ # Composants partagés réception/expédition (workflow-weight, workflow-waiting-list, workflow-liot-fields)
|
||||||
|
│ └── commun/ # Composants communs (update-weight)
|
||||||
|
├── composables/ # useApi, useWeighing, usePdfPrinter, useAppVersion, useLiotHandling, useFormDataLoading, useAddressSync, useWorkflowSteps
|
||||||
|
│ └── steps/ # useWeighingStep (logique étape pesée)
|
||||||
|
├── config/ # reception.config.ts, shipment.config.ts (WorkflowConfig)
|
||||||
|
├── types/ # workflow.ts (interfaces partagées WorkflowEntity, WorkflowConfig, StepDefinition)
|
||||||
|
├── services/ # Couche service avec DTOs typés dans services/dto/
|
||||||
|
│ └── workflow-service.ts # Factory service API (createWorkflowService)
|
||||||
|
├── stores/ # Pinia stores (reception, shipment, auth)
|
||||||
|
│ └── workflow-store.ts # Factory store (useWorkflowStoreLogic)
|
||||||
|
├── pages/ # Pages Nuxt (file-based routing)
|
||||||
|
├── layouts/ # Layout default : max-width 1050px
|
||||||
|
├── i18n/locales/ # Traductions (défaut: fr)
|
||||||
|
├── utils/ # Constants, zod-errors helpers
|
||||||
|
└── assets/css/ # Tailwind config, main.css (font Helvetica)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions backend
|
||||||
|
|
||||||
|
- Code en anglais ; "pont-bascule" est un terme métier conservé tel quel.
|
||||||
|
- Les opérations API Platform sont définies directement sur les entités Doctrine.
|
||||||
|
- Pas de classes Repository custom : utiliser `EntityManagerInterface` avec les repos par défaut.
|
||||||
|
- `config/reference.php` est auto-généré — ne pas modifier à la main.
|
||||||
|
- Endpoints toujours au pluriel (convention API Platform).
|
||||||
|
- Ne jamais créer de GET qui crée des ressources : utiliser POST + PATCH.
|
||||||
|
- Les noms de `Supplier`, `Customer` et `Carrier` sont automatiquement mis en majuscule via `mb_strtoupper` dans `setName()`.
|
||||||
|
|
||||||
|
## Conventions frontend
|
||||||
|
|
||||||
|
- SSR désactivé. Tailwind avec palette custom `primary` (ex: `bg-primary-500`).
|
||||||
|
- `useApi` (`composables/useApi.ts`) : méthodes `get/post/put/patch/delete` avec content-types par défaut.
|
||||||
|
- Toasts personnalisables via `toastErrorMessage`/`toastSuccessMessage` ou clés i18n `toastErrorKey`/`toastSuccessKey`.
|
||||||
|
- Utilise `useNuxtApp().$i18n` (pas `useI18n`) pour fonctionner hors setup.
|
||||||
|
- Validation formulaires avec Zod ; helpers dans `utils/zod-errors.ts`.
|
||||||
|
- Nav active : `NuxtLink` avec slot `custom`.
|
||||||
|
- PDFs : `usePdfPrinter` (receipt réception, rapport poids cases).
|
||||||
|
|
||||||
|
### Validation required & erreurs visuelles
|
||||||
|
- Les champs `required` utilisent l'attribut HTML natif forwardé via `v-bind="attrs"` dans les composants UI.
|
||||||
|
- La bordure rouge n'apparaît qu'après soumission grâce à la classe CSS `submitted` ajoutée sur le `<form>` au clic sur le bouton Valider (`@click="submitted = true"`).
|
||||||
|
- Règles CSS globales dans `main.css` : `.submitted :invalid` (bordure + texte rouge), `.submitted :has(:invalid) > label` et `.submitted label:has(:invalid)` (labels rouges).
|
||||||
|
- Pour les validations manuelles (checkboxes, radio groups), les messages d'erreur utilisent `invisible` (pas `hidden`) pour garder l'espace réservé et éviter les décalages de layout.
|
||||||
|
- Les dates de l'API sont renvoyées au format `Y-m-d H:i` ; les formulaires utilisent `.slice(0, 10)` pour extraire la date seule (compatible `<input type="date">`).
|
||||||
|
|
||||||
|
### Workflow réception/expédition (mutualisé)
|
||||||
|
- Factory service `createWorkflowService` et factory store `useWorkflowStoreLogic` pour éviter la duplication.
|
||||||
|
- Composables partagés : `useLiotHandling` (logique LIOT), `useFormDataLoading` (users, trucks, carriers), `useAddressSync` (sync adresse fournisseur/client).
|
||||||
|
- `useWeighing` : une seule fonction paramétrée pour réception et expédition (remplace `useWeighing` + `useWeighingShipment`).
|
||||||
|
- Configs workflow dans `config/reception.config.ts` et `config/shipment.config.ts` (étapes, labels pesée, filename PDF).
|
||||||
|
- `WorkflowWeight` composant partagé pour les étapes de pesée (remplace `reception-weight.vue` et `shipment-weight.vue`).
|
||||||
|
- `WorkflowWaitingList` composant partagé pour les listes en attente, avec support colonnes dynamiques, slot `actions`, et prop `showActions`.
|
||||||
|
|
||||||
|
## Domaine métier clé
|
||||||
|
|
||||||
|
### Réception (pesée pont-bascule)
|
||||||
|
- Entité principale `Reception` : `date_reception` (DateTimeImmutable, format lecture `Y-m-d H:i`, écriture `Y-m-d`), `identification_number` (auto `N-BR-####`), `current_step` (défaut 0), `is_valid` (défaut false).
|
||||||
|
- `Weight` (1-N avec Reception, cascade remove + orphanRemoval) : `type` (`gross`/`tare`), `dsd`, `weight`, `weighed_at`.
|
||||||
|
- Endpoint pesée : `/receptions/weigh` → `PontBasculeReading` (dsd, weight, weighedAt).
|
||||||
|
- Endpoint suppression : `DELETE /receptions/{id}` — supprime en cascade weights, pelletBuildings, bovines.
|
||||||
|
- Parsing payload pont-bascule : `Service/PontBasculePayloadDecoder.php`.
|
||||||
|
- Exception : `PontBasculeException` (messages en français, mappée 500).
|
||||||
|
- Store Pinia `reception.ts` = source de vérité pour la réception en cours.
|
||||||
|
- UI multi-étapes dans `pages/reception/[[id]].vue` basée sur `currentStep`.
|
||||||
|
- `PrePersist` : injecte l'heure courante sur `receptionDate` à la création ; `setReceptionDate` préserve l'heure existante au PATCH.
|
||||||
|
|
||||||
|
### Expédition
|
||||||
|
- Entité `Shipment` : même pattern que Reception, `shipment_date` (format lecture `Y-m-d H:i`, écriture `Y-m-d`).
|
||||||
|
- Endpoint suppression : `DELETE /shipments/{id}`.
|
||||||
|
- `PrePersist` : injecte l'heure courante sur `shipmentDate` ; `setShipmentDate` préserve l'heure au PATCH.
|
||||||
|
|
||||||
|
### LIOT (transport)
|
||||||
|
- Si carrier code = `LIOT` : afficher sélecteurs driver + vehicle, masquer saisie plaque manuelle.
|
||||||
|
- Liste véhicules filtrée par type de camion et transporteur.
|
||||||
|
- Le véhicule sélectionné alimente `license_plate`.
|
||||||
|
- Logique mutualisée dans `composables/useLiotHandling.ts`.
|
||||||
|
|
||||||
|
### Bovins & infrastructure
|
||||||
|
- `Bovine` : `nationalNumber` (unique), `receivedWeight`, `arrivalDate`, `buildingCase` (ManyToOne).
|
||||||
|
- `BuildingCase` a `bovines` (OneToMany).
|
||||||
|
- Rapport PDF cases : `GET /building_cases/{id}/weights-report` → template Twig, projection depuis `arrivalDate`, gain journalier fixe `1.3 kg/jour`.
|
||||||
|
|
||||||
|
### Données de référence
|
||||||
|
- `ReceptionType`, `MerchandiseType`, `PelletType`, `Building`, `Supplier` (avec `Address` via join table, `createdBy` → User), `Customer` (avec `Address` via join table, `createdBy` → User), `Truck`, `Carrier`, `Driver`, `Vehicle`.
|
||||||
|
- `Address` : champ `label` nullable (déprécié, retiré du front et du `address:write`), expose `fullAddress` via getter. `countryCode` par défaut `FR` côté front.
|
||||||
|
|
||||||
|
### Seed & fixtures
|
||||||
|
- Commande `app:seed` : seed infrastructure (statut, building_layout, building_case, building_case_position) puis bovins.
|
||||||
|
- Utilise des flush intermédiaires pour que les queries find fonctionnent sur les records fraîchement créés.
|
||||||
|
- `upsertAddress` cherche par `street|postalCode` (plus par `label`).
|
||||||
|
- Fixtures : `BuildingInfrastructureFixtures` + `BovineFixtures` (via dépendances `AppFixtures`).
|
||||||
|
|
||||||
|
## Environnement
|
||||||
|
|
||||||
|
- API base dev : `http://localhost:8080/api` (via `NUXT_PUBLIC_API_BASE` dans `frontend/.env`)
|
||||||
|
- CORS : Nelmio, configurable via `CORS_ALLOW_ORIGIN` dans `.env`
|
||||||
|
- Locale par défaut : `fr` — traductions dans `frontend/i18n/locales/fr.json`
|
||||||
|
- Docker env : `docker/.env.docker` (défaut) avec override possible via `docker/.env.docker.local`
|
||||||
90
DEPLOYMENT.md
Normal file
90
DEPLOYMENT.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Déploiement Ferme (release Gitea)
|
||||||
|
|
||||||
|
## 1) Premier déploiement
|
||||||
|
|
||||||
|
### Pré-requis système (Ubuntu)
|
||||||
|
1. Mettre à jour la machine
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y software-properties-common ca-certificates curl gnupg unzip git nginx
|
||||||
|
```
|
||||||
|
2. Installer PHP 8.4 + FPM + extensions
|
||||||
|
```bash
|
||||||
|
sudo add-apt-repository -y ppa:ondrej/php
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y \
|
||||||
|
php8.4 php8.4-fpm php8.4-cli php8.4-common \
|
||||||
|
php8.4-mbstring php8.4-xml php8.4-curl php8.4-intl \
|
||||||
|
php8.4-zip php8.4-gd php8.4-pgsql php8.4-opcache
|
||||||
|
```
|
||||||
|
3. Installer PostgreSQL (si la DB est locale)
|
||||||
|
```bash
|
||||||
|
sudo apt install -y postgresql postgresql-contrib
|
||||||
|
sudo -u postgres psql
|
||||||
|
```
|
||||||
|
Dans psql :
|
||||||
|
```sql
|
||||||
|
CREATE USER ferme_user WITH PASSWORD 'motdepassefort';
|
||||||
|
CREATE DATABASE ferme OWNER ferme_user;
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dossier de déploiement
|
||||||
|
1. Créer le dossier de déploiement
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /var/www/ferme
|
||||||
|
sudo chown -R malio:malio /var/www/ferme
|
||||||
|
```
|
||||||
|
2. Créer le fichier d’environnement
|
||||||
|
- Backend : `/var/www/ferme/.env`
|
||||||
|
- `APP_ENV=prod`
|
||||||
|
- `APP_DEBUG=0`
|
||||||
|
- `APP_SECRET=...`
|
||||||
|
- `DATABASE_URL=postgresql://ferme_user:motdepassefort@127.0.0.1:5432/ferme?serverVersion=16&charset=utf8`
|
||||||
|
- `JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem`
|
||||||
|
- `JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem`
|
||||||
|
- `JWT_PASSPHRASE=...`
|
||||||
|
- `COOKIE_SECURE=1`
|
||||||
|
- `PONT_BASCULE_BYPASS=false`
|
||||||
|
3. Générer les clés JWT
|
||||||
|
```bash
|
||||||
|
cd /var/www/ferme
|
||||||
|
mkdir -p config/jwt
|
||||||
|
php bin/console lexik:jwt:generate-keypair
|
||||||
|
```
|
||||||
|
4. Config Nginx (sous-domaine)<br>
|
||||||
|
Copier le fichier de conf /deploy/nginx/ferme.conf dans /etc/nginx/sites-available/ferme.conf
|
||||||
|
```bash
|
||||||
|
sudo ln -s /etc/nginx/sites-available/ferme.conf /etc/nginx/sites-enabled/ferme.conf
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
5. Installer le script de déploiement (disponible /scripts/deploy-release.sh)
|
||||||
|
```bash
|
||||||
|
sudo nano /usr/local/bin/deploy-ferme
|
||||||
|
sudo chmod +x /usr/local/bin/deploy-ferme
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) Déployer une release
|
||||||
|
|
||||||
|
1. Créer un tag sur `develop` (auto-tag `v0.0.X`)
|
||||||
|
2. Attendre que la release Gitea soit publiée
|
||||||
|
3. (Une seule fois) Donner les droits d'écriture à PHP sur `var/` via ACL
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y acl
|
||||||
|
sudo setfacl -R -m u:malio:rwx,g:www-data:rwx /var/www/ferme/var
|
||||||
|
sudo setfacl -R -m d:u:malio:rwx,d:g:www-data:rwx /var/www/ferme/var
|
||||||
|
```
|
||||||
|
4. Déployer la release
|
||||||
|
```bash
|
||||||
|
/usr/local/bin/deploy-ferme vX.Y.Z
|
||||||
|
```
|
||||||
|
Notes :
|
||||||
|
- Lancer le déploiement en tant que `malio` (ou `sudo -u malio`) pour éviter de casser les droits.
|
||||||
|
- Le script applique `umask 002` pour garder les fichiers group-writable (`www-data`).
|
||||||
|
- Le script force des droits d'exécution minimaux sur `/var/www` et `/var/www/ferme` pour éviter un blocage Nginx.
|
||||||
|
|
||||||
|
### Vérifications
|
||||||
|
- Front : `http://ferme.malio-dev.fr/`
|
||||||
|
- API : `http://ferme.malio-dev.fr/api/users`
|
||||||
|
- Login : `POST http://ferme.malio-dev.fr/api/login_check`
|
||||||
101
README.md
101
README.md
@@ -17,6 +17,32 @@ make install
|
|||||||
```
|
```
|
||||||
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
|
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
|
||||||
|
|
||||||
|
### Configuration global
|
||||||
|
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
||||||
|
|
||||||
|
Vérifier que dans le .env.local, vous avez :
|
||||||
|
* APP_SECRET (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));" et doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
||||||
|
* DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
|
* PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||||
|
* PONT_BASCULE_URL
|
||||||
|
* JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
||||||
|
* JWT_PUBLIC_KEY
|
||||||
|
* JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||||
|
* COOKIE_SECURE=0 (en dev 0 et en prod 1. Si c'est du http, laisser en 0)
|
||||||
|
|
||||||
|
Vérifier que dans le .env du dossier frontend, vous avez :
|
||||||
|
* NUXT_PUBLIC_API_BASE="http://localhost:8080/api"
|
||||||
|
|
||||||
|
### Configuration xdebug
|
||||||
|
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||||
|
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
||||||
|
* Name : ferme-docker
|
||||||
|
* Host : localhost
|
||||||
|
* Port : 8080
|
||||||
|
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
||||||
|
|
||||||
|
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
|
||||||
|
|
||||||
## Utilisation du projet
|
## Utilisation du projet
|
||||||
### Backend
|
### Backend
|
||||||
L'api est disponible sur http://localhost:8080/api
|
L'api est disponible sur http://localhost:8080/api
|
||||||
@@ -31,3 +57,78 @@ Pour le frontend, il suffit de taper la commande suivante qui va lancer le serve
|
|||||||
make dev-nuxt
|
make dev-nuxt
|
||||||
```
|
```
|
||||||
Le front sera accessible sur http://localhost:3000
|
Le front sera accessible sur http://localhost:3000
|
||||||
|
|
||||||
|
### Authentification
|
||||||
|
Ce projet utilise l'authentification JWT avec un cookie httpOnly (LexikJWTAuthenticationBundle).
|
||||||
|
Le frontend ne lit jamais directement le token, le navigateur envoie automatiquement le cookie.
|
||||||
|
|
||||||
|
### Login flow
|
||||||
|
- Frontend envoie les identifiants à:
|
||||||
|
- `POST /api/login_check`
|
||||||
|
- Backend returns:
|
||||||
|
- `204 No Content` (normal)
|
||||||
|
- `Set-Cookie: BEARER=...; HttpOnly`
|
||||||
|
- Le cookie est automatiquement envoyé pour les futures requêtes.
|
||||||
|
- La déconnexion utilise `POST /api/logout` et redirige vers `/login`.
|
||||||
|
|
||||||
|
### Fixtures
|
||||||
|
Pour lancer les fixtures (Attention sa purge la bdd complètement)
|
||||||
|
```bash
|
||||||
|
php bin/console doctrine:fixtures:load
|
||||||
|
```
|
||||||
|
|
||||||
|
Attention cette commande est dangereuse, à utiliser que pour les débuts de la prod ou en recette.
|
||||||
|
Dans un premier temps pour remplir les listes, vous pouvez lancer la commande symfony
|
||||||
|
```bash
|
||||||
|
php bin/console app:seed
|
||||||
|
```
|
||||||
|
La commande va faire une update ou une création en fonction des data existante.
|
||||||
|
|
||||||
|
## Livraison en recette
|
||||||
|
### Préparatifs
|
||||||
|
Avant de déployer, il faut penser à ajouter les variables d'env s'il y a des changements/modifications.
|
||||||
|
Le .env se trouve /var/www/ferme/.env
|
||||||
|
|
||||||
|
Le script de livraison est version dans le repo dans script/deploy-release.sh <br>
|
||||||
|
Sur la machine, il est disponible dans /usr/local/bin/deploy-ferme <br>
|
||||||
|
Pour le modifier, il faut copier le contenu du deploy-release.sh dans le deploy-ferme
|
||||||
|
### Livraison
|
||||||
|
Sur le serveur de recette, il suffit d'utiliser cette commande pour livrer
|
||||||
|
```bash
|
||||||
|
/usr/local/bin/deploy-ferme vX.Y.Z
|
||||||
|
```
|
||||||
|
## Commandes utiles
|
||||||
|
Pour restart le container
|
||||||
|
```bash
|
||||||
|
make restart
|
||||||
|
```
|
||||||
|
Pour lancer les TU
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
Pour accéder au container et lance des commandes
|
||||||
|
```bash
|
||||||
|
make shell
|
||||||
|
```
|
||||||
|
Pour clear le cache Symfony
|
||||||
|
```bash
|
||||||
|
make cache-clear
|
||||||
|
```
|
||||||
|
Faire une migration
|
||||||
|
```bash
|
||||||
|
make migration-migrate
|
||||||
|
```
|
||||||
|
Pour générer un password pour un user
|
||||||
|
```bash
|
||||||
|
make shell
|
||||||
|
php bin/console security:hash-password
|
||||||
|
```
|
||||||
|
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
||||||
|
```sql
|
||||||
|
INSERT INTO "user" (username, roles, password)
|
||||||
|
VALUES ('Mon user', '["ROLE_USER"]', 'Mon mdp hashé');
|
||||||
|
```
|
||||||
|
## Gestion des logs
|
||||||
|
Pour suivre les logs en temps réel :
|
||||||
|
* tail -f var/log/dev.log
|
||||||
|
* tail -f var/log/prod.log
|
||||||
|
|||||||
31
commit-msg
Normal file
31
commit-msg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MSG_FILE="${1}"
|
||||||
|
FIRST_LINE="$(head -n 1 "$MSG_FILE" | tr -d '\r')"
|
||||||
|
|
||||||
|
# Autoriser commits auto-générés par git
|
||||||
|
if [[ "$FIRST_LINE" =~ ^Merge\ ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Types autorisés (MINUSCULES uniquement)
|
||||||
|
# Optionnel: scope => feat(auth) : ...
|
||||||
|
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+'
|
||||||
|
|
||||||
|
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
|
||||||
|
echo "❌ Message de commit invalide."
|
||||||
|
echo ""
|
||||||
|
echo "➡️ Format attendu : <type>(<scope optionnel>) : <message>"
|
||||||
|
echo "➡️ Types autorisés (minuscules uniquement) :"
|
||||||
|
echo " build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Exemples :"
|
||||||
|
echo " feat : add login page"
|
||||||
|
echo " fix(auth) : prevent null token crash"
|
||||||
|
echo " docs : update README"
|
||||||
|
echo ""
|
||||||
|
echo "❌ Exemple refusé :"
|
||||||
|
echo " Feat : add login page"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
"doctrine/doctrine-bundle": "^3.2",
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
|
"dompdf/dompdf": "^3.1",
|
||||||
|
"lexik/jwt-authentication-bundle": "*",
|
||||||
|
"malio/ednotif-bundle": ">=0.0.4",
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
@@ -21,6 +24,8 @@
|
|||||||
"symfony/expression-language": "8.0.*",
|
"symfony/expression-language": "8.0.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "8.0.*",
|
"symfony/framework-bundle": "8.0.*",
|
||||||
|
"symfony/http-client": "8.0.*",
|
||||||
|
"symfony/monolog-bundle": "^4.0",
|
||||||
"symfony/property-access": "8.0.*",
|
"symfony/property-access": "8.0.*",
|
||||||
"symfony/property-info": "8.0.*",
|
"symfony/property-info": "8.0.*",
|
||||||
"symfony/runtime": "8.0.*",
|
"symfony/runtime": "8.0.*",
|
||||||
@@ -83,9 +88,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"doctrine/doctrine-fixtures-bundle": "^4.3",
|
||||||
"friendsofphp/php-cs-fixer": "^3.92",
|
"friendsofphp/php-cs-fixer": "^3.92",
|
||||||
"phpunit/phpunit": "^12.5",
|
"phpunit/phpunit": "^12.5",
|
||||||
"symfony/browser-kit": "8.0.*",
|
"symfony/browser-kit": "8.0.*",
|
||||||
"symfony/css-selector": "8.0.*"
|
"symfony/css-selector": "8.0.*",
|
||||||
|
"symfony/maker-bundle": "^1.65",
|
||||||
|
"symfony/stopwatch": "8.0.*",
|
||||||
|
"symfony/web-profiler-bundle": "8.0.*"
|
||||||
|
},
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://gitea.malio.fr/MALIO-DEV/ednotif-bundle"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2004
composer.lock
generated
2004
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle;
|
||||||
|
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||||
|
use Malio\EdnotifBundle\EdnotifBundle;
|
||||||
|
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
|
use Symfony\Bundle\MakerBundle\MakerBundle;
|
||||||
|
use Symfony\Bundle\MonologBundle\MonologBundle;
|
||||||
|
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||||
|
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||||
|
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
FrameworkBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
TwigBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
SecurityBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
DoctrineBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
NelmioCorsBundle::class => ['all' => true],
|
||||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
|
ApiPlatformBundle::class => ['all' => true],
|
||||||
|
MonologBundle::class => ['all' => true],
|
||||||
|
EdnotifBundle::class => ['all' => true],
|
||||||
|
WebProfilerBundle::class => ['dev' => true],
|
||||||
|
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
MakerBundle::class => ['dev' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,3 +5,8 @@ api_platform:
|
|||||||
stateless: true
|
stateless: true
|
||||||
cache_headers:
|
cache_headers:
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
|
formats:
|
||||||
|
json: ['application/json']
|
||||||
|
jsonld: ['application/ld+json']
|
||||||
|
patch_formats:
|
||||||
|
json: ['application/merge-patch+json']
|
||||||
|
|||||||
13
config/packages/ednotif.yaml
Normal file
13
config/packages/ednotif.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
ednotif:
|
||||||
|
guichet_wsdl: 'https://ws-reswel-elv.equade.fr/wsguichet/WsGuichet?wsdl'
|
||||||
|
metier_wsdl: 'https://ws-ednotif.equade.fr/wsIpBNotif/wsIpBNotif?wsdl'
|
||||||
|
exploitation_code: '%env(string:EDNOTIF_EXPLOITATION_CODE)%'
|
||||||
|
exploitation_number: '%env(string:EDNOTIF_EXPLOITATION_NUMERO)%'
|
||||||
|
exploitation_country_code: 'FR'
|
||||||
|
login: '%env(string:EDNOTIF_LOGIN)%'
|
||||||
|
password: '%env(string:EDNOTIF_PASSWORD)%'
|
||||||
|
token_ttl_seconds: 900
|
||||||
|
soap_options:
|
||||||
|
trace: false
|
||||||
|
exceptions: true
|
||||||
|
connection_timeout: 15
|
||||||
20
config/packages/lexik_jwt_authentication.yaml
Normal file
20
config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
lexik_jwt_authentication:
|
||||||
|
secret_key: '%kernel.project_dir%/config/jwt/private.pem'
|
||||||
|
public_key: '%kernel.project_dir%/config/jwt/public.pem'
|
||||||
|
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||||
|
token_ttl: 86400
|
||||||
|
token_extractors:
|
||||||
|
authorization_header:
|
||||||
|
enabled: true
|
||||||
|
prefix: Bearer
|
||||||
|
name: Authorization
|
||||||
|
cookie:
|
||||||
|
enabled: true
|
||||||
|
name: BEARER
|
||||||
|
set_cookies:
|
||||||
|
BEARER:
|
||||||
|
lifetime: 86400
|
||||||
|
path: /
|
||||||
|
samesite: lax
|
||||||
|
secure: '%env(bool:COOKIE_SECURE)%'
|
||||||
|
httpOnly: true
|
||||||
28
config/packages/monolog.yaml
Normal file
28
config/packages/monolog.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
monolog:
|
||||||
|
channels: [deprecation]
|
||||||
|
|
||||||
|
when@dev:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
channels: ["!event"]
|
||||||
|
console:
|
||||||
|
type: console
|
||||||
|
process_psr_3_messages: false
|
||||||
|
channels: ["!event", "!doctrine", "!console"]
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
monolog:
|
||||||
|
handlers:
|
||||||
|
main:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
|
level: debug
|
||||||
|
channels: ["!deprecation"]
|
||||||
|
deprecation:
|
||||||
|
type: stream
|
||||||
|
channels: [deprecation]
|
||||||
|
path: "%kernel.logs_dir%/deprecations.log"
|
||||||
@@ -4,6 +4,7 @@ nelmio_cors:
|
|||||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
allow_headers: ['Content-Type', 'Authorization']
|
allow_headers: ['Content-Type', 'Authorization']
|
||||||
|
allow_credentials: true
|
||||||
expose_headers: ['Link']
|
expose_headers: ['Link']
|
||||||
max_age: 3600
|
max_age: 3600
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
4
config/packages/prod/api_platform.yaml
Normal file
4
config/packages/prod/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
api_platform:
|
||||||
|
enable_docs: false
|
||||||
|
enable_swagger: false
|
||||||
|
enable_swagger_ui: false
|
||||||
@@ -1,20 +1,43 @@
|
|||||||
security:
|
security:
|
||||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
|
App\Entity\User: 'auto'
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
users_in_memory: { memory: null }
|
app_user_provider:
|
||||||
|
entity:
|
||||||
|
class: App\Entity\User
|
||||||
|
property: username
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
# Ensure dev tools and static assets are always allowed
|
# Ensure dev tools and static assets are always allowed
|
||||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
security: false
|
security: false
|
||||||
main:
|
login:
|
||||||
lazy: true
|
pattern: ^/login_check
|
||||||
provider: users_in_memory
|
stateless: true
|
||||||
|
provider: app_user_provider
|
||||||
|
json_login:
|
||||||
|
check_path: /login_check
|
||||||
|
username_path: username
|
||||||
|
password_path: password
|
||||||
|
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||||
|
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||||
|
api:
|
||||||
|
pattern: ^/
|
||||||
|
stateless: true
|
||||||
|
provider: app_user_provider
|
||||||
|
jwt: ~
|
||||||
|
logout:
|
||||||
|
path: /api/logout
|
||||||
|
target: /login
|
||||||
|
enable_csrf: false
|
||||||
|
delete_cookies:
|
||||||
|
BEARER:
|
||||||
|
path: /
|
||||||
|
|
||||||
# Activate different ways to authenticate:
|
# Activate different ways to authenticate:
|
||||||
# https://symfony.com/doc/current/security.html#the-firewall
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
@@ -24,8 +47,16 @@ security:
|
|||||||
|
|
||||||
# Note: Only the *first* matching rule is applied
|
# Note: Only the *first* matching rule is applied
|
||||||
access_control:
|
access_control:
|
||||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
# Login JWT
|
||||||
# - { path: ^/profile, roles: ROLE_USER }
|
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
||||||
|
# Liste des users en lecture publique
|
||||||
|
- { path: ^/api/users, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||||
|
# Doc API (swagger) en public
|
||||||
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
|
# Version de l'application en public
|
||||||
|
- { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||||
|
# Tout le reste nécessite un JWT
|
||||||
|
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
|
|||||||
7
config/packages/web_profiler.yaml
Normal file
7
config/packages/web_profiler.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
when@dev:
|
||||||
|
web_profiler:
|
||||||
|
toolbar: true
|
||||||
|
|
||||||
|
framework:
|
||||||
|
profiler:
|
||||||
|
collect_serializer_data: true
|
||||||
1463
config/reference.php
1463
config/reference.php
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
resource: .
|
resource: .
|
||||||
type: api_platform
|
type: api_platform
|
||||||
prefix: /
|
prefix: /api
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
_security_logout:
|
_security_logout:
|
||||||
resource: security.route_loader.logout
|
resource: security.route_loader.logout
|
||||||
type: service
|
type: service
|
||||||
|
|
||||||
|
api_login:
|
||||||
|
path: /login_check
|
||||||
|
methods: [POST]
|
||||||
|
|||||||
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
when@dev:
|
||||||
|
web_profiler_wdt:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||||
|
prefix: /_wdt
|
||||||
|
|
||||||
|
web_profiler_profiler:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||||
|
prefix: /_profiler
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
|
|
||||||
|
imports:
|
||||||
|
- { resource: version.yaml }
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
_defaults:
|
_defaults:
|
||||||
@@ -19,5 +22,10 @@ services:
|
|||||||
App\:
|
App\:
|
||||||
resource: '../src/'
|
resource: '../src/'
|
||||||
|
|
||||||
|
App\Service\PontBasculeService:
|
||||||
|
arguments:
|
||||||
|
$endpoint: '%env(PONT_BASCULE_URL)%'
|
||||||
|
$bypass: '%env(bool:PONT_BASCULE_BYPASS)%'
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|||||||
2
config/version.yaml
Normal file
2
config/version.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
parameters:
|
||||||
|
app.version: '0.0.73'
|
||||||
43
deploy/nginx/ferme.conf
Normal file
43
deploy/nginx/ferme.conf
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ferme.malio-dev.fr;
|
||||||
|
|
||||||
|
root /var/www/ferme/frontend/.output/public;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
root /var/www/ferme/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /bundles/ {
|
||||||
|
root /var/www/ferme/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/login_check {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/ferme/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/ferme/public;
|
||||||
|
fastcgi_param SCRIPT_NAME /index.php;
|
||||||
|
fastcgi_param PATH_INFO /login_check;
|
||||||
|
fastcgi_param REQUEST_URI /login_check;
|
||||||
|
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/ferme/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/ferme/public;
|
||||||
|
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
web:
|
php:
|
||||||
container_name: php-${DOCKER_APP_NAME}-apache
|
container_name: php-${DOCKER_APP_NAME}-fpm
|
||||||
build:
|
build:
|
||||||
context: ./docker/php
|
context: ./docker/php
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -14,24 +14,34 @@ services:
|
|||||||
XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal}
|
XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal}
|
||||||
XDEBUG_CONFIG: client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} client_port=9003
|
XDEBUG_CONFIG: client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} client_port=9003
|
||||||
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
|
COMPOSER_HOME: /tmp/composer
|
||||||
|
COMPOSER_CACHE_DIR: /tmp/composer/cache
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/var/www/html
|
- ./:/var/www/html
|
||||||
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
||||||
- ~/.config:/var/www/.config # Pour la config de yarn
|
- ~/.config:/var/www/.config # Pour la config de yarn
|
||||||
- ~/.composer:/var/www/.composer # Pour la config de composer
|
- ~/.composer:/var/www/.composer # Pour la config de composer
|
||||||
- ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
|
- ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
|
||||||
- ./docker/php/config/vhost.conf:/etc/apache2/sites-available/000-default.conf
|
|
||||||
- ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
- ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
- ./LOG:/var/www/html/LOG
|
- ./LOG:/var/www/html/LOG
|
||||||
- ./LOG/logs_apache:/var/log/apache2/
|
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: nginx-${DOCKER_APP_NAME}
|
||||||
|
depends_on:
|
||||||
|
- php
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www/html:ro
|
||||||
|
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
restart: unless-stopped
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ POSTGRES_USER=root
|
|||||||
POSTGRES_PASSWORD=root
|
POSTGRES_PASSWORD=root
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||||
|
CURRENT_UID=1004
|
||||||
|
CURRENT_GID=1004
|
||||||
|
|||||||
52
docker/nginx/conf.d/ferme.conf
Normal file
52
docker/nginx/conf.d/ferme.conf
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /var/www/html/frontend/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location ^~ /api/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /_wdt/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /_profiler/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /bundles/ {
|
||||||
|
root /var/www/html/public;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/login_check {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_param SCRIPT_NAME /index.php;
|
||||||
|
fastcgi_param PATH_INFO /login_check;
|
||||||
|
fastcgi_param REQUEST_URI /login_check;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||||
|
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
ARG DOCKER_PHP_VERSION
|
ARG DOCKER_PHP_VERSION
|
||||||
|
|
||||||
FROM php:${DOCKER_PHP_VERSION}-apache-bullseye
|
FROM php:${DOCKER_PHP_VERSION}-fpm-bullseye
|
||||||
|
|
||||||
ARG DOCKER_NODE_VERSION
|
ARG DOCKER_NODE_VERSION
|
||||||
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
||||||
@@ -102,13 +102,14 @@ RUN docker-php-ext-enable opcache
|
|||||||
RUN rm -rf /var/cache/apk/* && rm -rf /tmp/* && \
|
RUN rm -rf /var/cache/apk/* && rm -rf /tmp/* && \
|
||||||
curl --insecure https://getcomposer.org/composer.phar -o /usr/bin/composer && chmod +x /usr/bin/composer
|
curl --insecure https://getcomposer.org/composer.phar -o /usr/bin/composer && chmod +x /usr/bin/composer
|
||||||
|
|
||||||
|
# cache Composer pour www-data
|
||||||
|
RUN mkdir -p /var/www/.composer/cache/vcs \
|
||||||
|
&& chown -R www-data:www-data /var/www/.composer
|
||||||
|
ENV COMPOSER_HOME=/var/www/.composer
|
||||||
|
|
||||||
# Création de la structure du projet
|
# Création de la structure du projet
|
||||||
RUN mkdir /var/www/html/LOG
|
RUN mkdir /var/www/html/LOG
|
||||||
|
|
||||||
# Activation du module pour Apache2 proxy_http et rewrite
|
|
||||||
RUN a2enmod proxy_http && \
|
|
||||||
a2enmod rewrite
|
|
||||||
|
|
||||||
###> User ###
|
###> User ###
|
||||||
ARG CURRENT_UID
|
ARG CURRENT_UID
|
||||||
ARG CURRENT_GID
|
ARG CURRENT_GID
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<VirtualHost *:80>
|
|
||||||
DocumentRoot /var/www/html
|
|
||||||
|
|
||||||
AliasMatch "^/api(/.*)?" "/var/www/html/public$1"
|
|
||||||
<Directory /var/www/html/public>
|
|
||||||
Options FollowSymLinks
|
|
||||||
AllowOverride All
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
AliasMatch "^(/.*)?" "/var/www/html/frontend/dist$1"
|
|
||||||
<Directory /var/www/html/frontend/dist>
|
|
||||||
AllowOverride All
|
|
||||||
Order allow,deny
|
|
||||||
Allow from All
|
|
||||||
|
|
||||||
RewriteEngine on
|
|
||||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
|
||||||
RewriteCond %{REQUEST_FILENAME} -d
|
|
||||||
RewriteRule ^ - [L]
|
|
||||||
RewriteRule ^ index.html [L]
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
ErrorLog "${APACHE_LOG_DIR}/error.log"
|
|
||||||
CustomLog "${APACHE_LOG_DIR}/access.log" combined
|
|
||||||
</VirtualHost>
|
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtPage/>
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { load } = useAppVersion()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
23
frontend/assets/css/main.css
Normal file
23
frontend/assets/css/main.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.submitted :invalid {
|
||||||
|
@apply border-red-500 text-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitted :has(:invalid) > label {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitted label:has(:invalid) {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
frontend/assets/css/toast.css
Normal file
18
frontend/assets/css/toast.css
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.iziToast {
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iziToast > .iziToast-body {
|
||||||
|
padding: 18px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iziToast > .iziToast-body .iziToast-title {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iziToast > .iziToast-body .iziToast-message {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
149
frontend/components/address.vue
Normal file
149
frontend/components/address.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validateForm">
|
||||||
|
<div class="flex items-center mb-11 justify-between relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
|
<Icon @click="goBack" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ props.address ? "Modification d'une adresse" : "Ajout d'une adresse" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-y-16 gap-x-[200px] mb-16">
|
||||||
|
<UiTextInput id="address-street" v-model="form.street" label="Rue" required />
|
||||||
|
<UiTextInput id="address-street2" v-model="form.street2" label="Complément" />
|
||||||
|
<UiTextInput id="address-postalCode" v-model="form.postalCode" label="Code postal" required />
|
||||||
|
<UiSelect
|
||||||
|
id="address-city"
|
||||||
|
v-model="form.city"
|
||||||
|
label="Ville"
|
||||||
|
:options="communeOptions"
|
||||||
|
:loading="isLoadingCities"
|
||||||
|
:disabled="communes.length === 0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiTextInput id="address-country" v-model="form.countryCode" label="Pays (code)" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AddressPayload } from "~/services/address"
|
||||||
|
import { getCommunesByPostalCode, type CommuneData } from "~/services/geo"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type?: "supplier" | "customer",
|
||||||
|
address?: AddressPayload | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
const communes = ref<CommuneData[]>([])
|
||||||
|
const isLoadingCities = ref(false)
|
||||||
|
|
||||||
|
const communeOptions = computed(() =>
|
||||||
|
communes.value.map(c => ({ value: c.nom, label: c.nom }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const emptyForm = (): AddressPayload => ({
|
||||||
|
street: "",
|
||||||
|
street2: null,
|
||||||
|
postalCode: "",
|
||||||
|
city: "",
|
||||||
|
countryCode: "FR",
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive<AddressPayload>(emptyForm())
|
||||||
|
|
||||||
|
const backPath = computed(() => {
|
||||||
|
if (props.type === "customer") {
|
||||||
|
const customerId = Number(route.query.customerId)
|
||||||
|
return Number.isFinite(customerId) && customerId > 0
|
||||||
|
? `/admin/customer/${customerId}`
|
||||||
|
: "/admin/customer/customer-list"
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierId = Number(route.query.supplierId)
|
||||||
|
return Number.isFinite(supplierId) && supplierId > 0
|
||||||
|
? `/admin/supplier/${supplierId}`
|
||||||
|
: "/admin/supplier/supplier-list"
|
||||||
|
})
|
||||||
|
|
||||||
|
const hydrateForm = (address?: AddressPayload | null) => {
|
||||||
|
const data = address ?? emptyForm()
|
||||||
|
form.street = data.street ?? ""
|
||||||
|
form.street2 = data.street2 ?? null
|
||||||
|
form.postalCode = data.postalCode ?? ""
|
||||||
|
form.city = data.city ?? ""
|
||||||
|
form.countryCode = data.countryCode || "FR"
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.address,
|
||||||
|
(addr) => {
|
||||||
|
hydrateForm(addr)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.postalCode,
|
||||||
|
(cp) => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
|
||||||
|
if (!cp || cp.length < 5) {
|
||||||
|
communes.value = []
|
||||||
|
form.city = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp.length === 5) {
|
||||||
|
debounceTimer = setTimeout(async () => {
|
||||||
|
isLoadingCities.value = true
|
||||||
|
const previousCity = form.city
|
||||||
|
try {
|
||||||
|
communes.value = await getCommunesByPostalCode(cp)
|
||||||
|
|
||||||
|
if (communes.value.length === 1) {
|
||||||
|
form.city = communes.value[0].nom
|
||||||
|
} else if (communes.value.some(c => c.nom === previousCity)) {
|
||||||
|
form.city = previousCity
|
||||||
|
} else {
|
||||||
|
form.city = ''
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoadingCities.value = false
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
if (isLoading.value) return
|
||||||
|
emit("validate", {...form})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push(backPath.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'validate', form: AddressPayload): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
29
frontend/components/card-link.vue
Normal file
29
frontend/components/card-link.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLink :to="link">
|
||||||
|
<div class="w-[300px] h-[216px] border border-primary-700 rounded-lg p-6 flex flex-col justify-between gap-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="rounded-full w-[80px] h-[80px] bg-[#D9D9D9] flex justify-center items-center">
|
||||||
|
<Icon :name="iconName" class="!text-primary-700" size="44" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Icon name="mdi:plus" style="color: black" size="44" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uppercase font-bold">
|
||||||
|
<p class="text-3xl text-primary-700">
|
||||||
|
<slot name="label">{{ label }}</slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
link: string
|
||||||
|
iconName: string
|
||||||
|
label: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
65
frontend/components/commun/update-weight.vue
Normal file
65
frontend/components/commun/update-weight.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="grid grid-cols-3 gap-x-40 gap-y-8 mb-8">
|
||||||
|
<UiNumberInput
|
||||||
|
:key="localWeight.type"
|
||||||
|
:label="'POIDS'"
|
||||||
|
labelClass="font-bold uppercase text-xl "
|
||||||
|
v-model="localWeight.weight"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
:min="0"
|
||||||
|
wrapper-class="flex-col"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiDateInput
|
||||||
|
label="Date de pesée"
|
||||||
|
v-model="localWeight.weighedAt"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiNumberInput
|
||||||
|
label="Dsd"
|
||||||
|
class="col-start-2"
|
||||||
|
labelClass="font-bold uppercase"
|
||||||
|
v-model="localWeight.dsd"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
wrapper-class="flex-col"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {WeightEntryData} from '~/services/dto/weight-data'
|
||||||
|
import {reactive, watch} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: WeightEntryData
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: WeightEntryData): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localWeight = reactive<WeightEntryData>({...props.modelValue})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value) => {
|
||||||
|
Object.assign(localWeight, value)
|
||||||
|
},
|
||||||
|
{deep: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
localWeight,
|
||||||
|
(value) => {
|
||||||
|
emit('update:modelValue', {...value})
|
||||||
|
},
|
||||||
|
{deep: true}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
202
frontend/components/reception/reception-bovine-received.vue
Normal file
202
frontend/components/reception/reception-bovine-received.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
||||||
|
class="flex flex-col gap-16"
|
||||||
|
@submit.prevent="goNext"
|
||||||
|
>
|
||||||
|
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
|
||||||
|
<div
|
||||||
|
class="flex flex-row gap-8 items-center w-full">
|
||||||
|
<div
|
||||||
|
v-for="type in bovineType"
|
||||||
|
:key="type.id"
|
||||||
|
class="mt-8 flex flex-row mb-2 w-full">
|
||||||
|
<UiNumberInput
|
||||||
|
:id="type.id"
|
||||||
|
:label="type.label"
|
||||||
|
:code="type.code"
|
||||||
|
v-model="bovineQuantities[String(type.id)]"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
class="max-w-[150px]"
|
||||||
|
wrapper-class="gap-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-8 flex flex-row mb-2 gap-6">
|
||||||
|
<UiNumberInput
|
||||||
|
label="Autres"
|
||||||
|
v-model="otherQuantity"
|
||||||
|
class="max-w-[80px]"
|
||||||
|
wrapper-class="gap-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-red-500 text-sm" :class="showBovineError ? '' : 'invisible'">
|
||||||
|
Veuillez saisir au moins une race bovine.
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {BovineTypeData} from "~/services/dto/bovine-type-data";
|
||||||
|
import {getBovineTypeList} from "~/services/bovine-type";
|
||||||
|
import {RECEPTION_TYPE_CODES} from "~/utils/constants";
|
||||||
|
import {useReceptionStore} from '~/stores/reception'
|
||||||
|
import {
|
||||||
|
createReceptionBovine,
|
||||||
|
deleteReceptionBovine,
|
||||||
|
getReceptionBovineList,
|
||||||
|
updateReceptionBovine
|
||||||
|
} from "~/services/reception-bovine";
|
||||||
|
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const isLoadingBovineType = ref(false)
|
||||||
|
const bovineType = ref<BovineTypeData[]>([])
|
||||||
|
const receptionStore = useReceptionStore()
|
||||||
|
const showBovineError = ref(false)
|
||||||
|
const bovineQuantities = reactive<Record<string, number | null>>({})
|
||||||
|
const otherQuantity = ref<number | null>(0)
|
||||||
|
const receptionId = computed(() => receptionStore.current?.id ?? null)
|
||||||
|
const receptionIri = computed(() =>
|
||||||
|
receptionId.value ? `/api/receptions/${receptionId.value}` : null
|
||||||
|
)
|
||||||
|
const totalBovines = computed(() => {
|
||||||
|
const base = Object.values(bovineQuantities).reduce((sum, value) => {
|
||||||
|
return sum + (value ?? 0)
|
||||||
|
}, 0)
|
||||||
|
return base + (otherQuantity.value ?? 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadBovineType = async () => {
|
||||||
|
isLoadingBovineType.value = true
|
||||||
|
try {
|
||||||
|
bovineType.value = await getBovineTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingBovineType.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadBovineType()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => receptionId.value, () => bovineType.value],
|
||||||
|
async ([id, types]) => {
|
||||||
|
if (!id || !receptionIri.value || types.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionMap: Record<string, number | null> = {}
|
||||||
|
for (const type of types) {
|
||||||
|
selectionMap[String(type.id)] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await getReceptionBovineList(receptionIri.value)
|
||||||
|
for (const selection of existing) {
|
||||||
|
const bovineTypeId = String(selection.bovineType.id)
|
||||||
|
selectionMap[bovineTypeId] = selection.quantity ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(bovineQuantities)) {
|
||||||
|
delete bovineQuantities[key]
|
||||||
|
}
|
||||||
|
Object.assign(bovineQuantities, selectionMap)
|
||||||
|
|
||||||
|
const existingOther = receptionStore.current?.bovineDetail
|
||||||
|
const parsedOther =
|
||||||
|
typeof existingOther === 'string' && existingOther.trim() !== ''
|
||||||
|
? Number(existingOther)
|
||||||
|
: 0
|
||||||
|
otherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function syncBovineSelections(receptionIri: string) {
|
||||||
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
|
const existingMap = new Map<string, { id: number; quantity: number | null }>()
|
||||||
|
|
||||||
|
for (const selection of existing) {
|
||||||
|
const bovineTypeId = String(selection.bovineType.id)
|
||||||
|
existingMap.set(bovineTypeId, {
|
||||||
|
id: selection.id,
|
||||||
|
quantity: selection.quantity ?? 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime les entrées supprimées ou modifiées
|
||||||
|
for (const [bovineTypeId, entry] of existingMap.entries()) {
|
||||||
|
const selectedQuantity = bovineQuantities[bovineTypeId] ?? 0
|
||||||
|
if (!selectedQuantity) {
|
||||||
|
await deleteReceptionBovine(entry.id)
|
||||||
|
existingMap.delete(bovineTypeId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedQuantity !== entry.quantity) {
|
||||||
|
await updateReceptionBovine(entry.id, {quantity: selectedQuantity})
|
||||||
|
existingMap.set(bovineTypeId, {
|
||||||
|
id: entry.id,
|
||||||
|
quantity: selectedQuantity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crée les entrées manquantes
|
||||||
|
for (const [bovineTypeId, quantity] of Object.entries(bovineQuantities)) {
|
||||||
|
if (!quantity) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (existingMap.has(bovineTypeId)) {
|
||||||
|
// Déjà à jour
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await createReceptionBovine({
|
||||||
|
reception: receptionIri,
|
||||||
|
bovineType: `/api/bovine_types/${bovineTypeId}`,
|
||||||
|
quantity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goNext() {
|
||||||
|
if (!receptionStore.current || !receptionIri.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showBovineError.value = false
|
||||||
|
|
||||||
|
if (totalBovines.value === 0) {
|
||||||
|
showBovineError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalBovines.value > 52) {
|
||||||
|
toast.error({
|
||||||
|
title: 'Erreur',
|
||||||
|
message: ('Le total des bovins ne peut pas dépasser 52.')
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = receptionStore.current.currentStep + 1
|
||||||
|
await syncBovineSelections(receptionIri.value)
|
||||||
|
|
||||||
|
await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
|
merchandiseType: null,
|
||||||
|
merchandiseDetail: null,
|
||||||
|
bovineDetail: otherQuantity.value ? String(otherQuantity.value) : null,
|
||||||
|
currentStep: nextStep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
282
frontend/components/reception/reception-form.vue
Normal file
282
frontend/components/reception/reception-form.vue
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
||||||
|
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Réception</h1>
|
||||||
|
<UiSelect
|
||||||
|
id="reception-user"
|
||||||
|
v-model="form.userId"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:options="users.map((user) => ({
|
||||||
|
value: String(user.id),
|
||||||
|
label: user.username
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingUsers"
|
||||||
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiDateInput
|
||||||
|
id="reception-date"
|
||||||
|
v-model="form.receptionDate"
|
||||||
|
label="Date de réception"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="reception-type"
|
||||||
|
v-model="form.receptionTypeId"
|
||||||
|
label="Type de réception"
|
||||||
|
:options="receptionTypes.map((type) => ({
|
||||||
|
value: String(type.id),
|
||||||
|
label: type.label
|
||||||
|
}))"
|
||||||
|
wrapper-class="col-start-1 row-start-4"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="reception-supplier"
|
||||||
|
v-model="form.supplierId"
|
||||||
|
label="Fournisseur"
|
||||||
|
:options="suppliers.map((supplier) => ({
|
||||||
|
value: String(supplier.id),
|
||||||
|
label: supplier.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingSuppliers"
|
||||||
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="reception-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
label="Adresse"
|
||||||
|
:options="addressOptions"
|
||||||
|
:disabled="isLoadingSuppliers || ownerAddresses.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="reception-truck"
|
||||||
|
v-model="form.truckId"
|
||||||
|
label="Camion"
|
||||||
|
:options="trucks.map((truck) => ({
|
||||||
|
value: String(truck.id),
|
||||||
|
label: truck.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingTrucks"
|
||||||
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="reception-carrier"
|
||||||
|
v-model="form.carrierId"
|
||||||
|
label="Transporteur"
|
||||||
|
:options="carriers.map((carrier) => ({
|
||||||
|
value: String(carrier.id),
|
||||||
|
label: carrier.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingCarriers"
|
||||||
|
select-class="h-[34px]"
|
||||||
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
id="reception-vehicle"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4 h-[64px]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="reception-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
wrapper-class="col-start-2 row-start-5"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
|
import { useFormDataLoading } from '~/composables/useFormDataLoading'
|
||||||
|
import { useLiotHandling } from '~/composables/useLiotHandling'
|
||||||
|
import { useAddressSync } from '~/composables/useAddressSync'
|
||||||
|
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||||
|
import { getReceptionTypeList } from '~/services/reception-type'
|
||||||
|
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||||
|
import { getSupplierList } from '~/services/supplier'
|
||||||
|
import { RECEPTION_TYPE_CODES } from '~/utils/constants'
|
||||||
|
import { deleteReceptionBovine, getReceptionBovineList } from '~/services/reception-bovine'
|
||||||
|
import type { ReceptionFormData } from '~/services/dto/reception-data'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const receptionStore = useReceptionStore()
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
|
const form = reactive<ReceptionFormData>({
|
||||||
|
licensePlate: '',
|
||||||
|
receptionDate: new Date().toISOString().slice(0, 10),
|
||||||
|
receptionTypeId: '',
|
||||||
|
userId: '',
|
||||||
|
supplierId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||||
|
const suppliers = ref<SupplierData[]>([])
|
||||||
|
const isLoadingSuppliers = ref(false)
|
||||||
|
|
||||||
|
const { users, trucks, carriers, isLoadingUsers, isLoadingTrucks, isLoadingCarriers, loadCommonData } =
|
||||||
|
useFormDataLoading(form)
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLiotCarrier, filteredDrivers, filteredVehicles,
|
||||||
|
isLoadingDrivers, isLoadingVehicles, allowAnyLicensePlate,
|
||||||
|
loadDrivers, loadVehicles
|
||||||
|
} = useLiotHandling(form, carriers, isHydrating)
|
||||||
|
|
||||||
|
const supplierIdRef = computed(() => form.supplierId)
|
||||||
|
const { ownerAddresses, addressOptions } = useAddressSync(form, supplierIdRef, suppliers)
|
||||||
|
|
||||||
|
const selectedReceptionType = computed(() =>
|
||||||
|
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearReceptionBovines = async (receptionIri: string) => {
|
||||||
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
|
for (const selection of existing) {
|
||||||
|
await deleteReceptionBovine(selection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSuppliers = async () => {
|
||||||
|
isLoadingSuppliers.value = true
|
||||||
|
try {
|
||||||
|
suppliers.value = await getSupplierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingSuppliers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => receptionStore.current,
|
||||||
|
(reception) => {
|
||||||
|
isHydrating.value = true
|
||||||
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
|
form.receptionDate = reception?.receptionDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
|
||||||
|
form.receptionTypeId = reception?.receptionType?.id ? String(reception.receptionType.id) : ''
|
||||||
|
form.userId = reception?.user?.id ? String(reception.user.id) : form.userId
|
||||||
|
form.supplierId = reception?.supplier?.id ? String(reception.supplier.id) : ''
|
||||||
|
form.addressId = reception?.address?.id ? String(reception.address.id) : ''
|
||||||
|
form.truckId = reception?.truck?.id ? String(reception.truck.id) : ''
|
||||||
|
form.carrierId = reception?.carrier?.id ? String(reception.carrier.id) : ''
|
||||||
|
form.driverId = reception?.driver?.id ? String(reception.driver.id) : ''
|
||||||
|
isHydrating.value = false
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
receptionTypes.value = await getReceptionTypeList()
|
||||||
|
await loadSuppliers()
|
||||||
|
await loadCommonData()
|
||||||
|
await loadDrivers()
|
||||||
|
await loadVehicles()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
|
const normalizedReceptionDate = form.receptionDate.trim()
|
||||||
|
const normalizedReceptionTypeId = form.receptionTypeId.trim()
|
||||||
|
const normalizedUserId = form.userId.trim()
|
||||||
|
const normalizedSupplierId = form.supplierId.trim()
|
||||||
|
const normalizedAddressId = form.addressId.trim()
|
||||||
|
const normalizedTruckId = form.truckId.trim()
|
||||||
|
const normalizedCarrierId = form.carrierId.trim()
|
||||||
|
const normalizedDriverId = form.driverId.trim()
|
||||||
|
|
||||||
|
const receptionTypeIri = normalizedReceptionTypeId ? `/api/reception_types/${normalizedReceptionTypeId}` : null
|
||||||
|
const userIri = normalizedUserId ? `/api/users/${normalizedUserId}` : null
|
||||||
|
const supplierIri = normalizedSupplierId ? `/api/suppliers/${normalizedSupplierId}` : null
|
||||||
|
const addressIri = normalizedAddressId ? `/api/addresses/${normalizedAddressId}` : null
|
||||||
|
const truckIri = normalizedTruckId ? `/api/trucks/${normalizedTruckId}` : null
|
||||||
|
const carrierIri = normalizedCarrierId ? `/api/carriers/${normalizedCarrierId}` : null
|
||||||
|
const driverIri = normalizedDriverId ? `/api/drivers/${normalizedDriverId}` : null
|
||||||
|
|
||||||
|
const basePayload = {
|
||||||
|
licensePlate: normalizedLicensePlate,
|
||||||
|
receptionDate: normalizedReceptionDate,
|
||||||
|
receptionType: receptionTypeIri,
|
||||||
|
user: userIri,
|
||||||
|
supplier: supplierIri,
|
||||||
|
address: addressIri,
|
||||||
|
truck: truckIri,
|
||||||
|
carrier: carrierIri
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...basePayload,
|
||||||
|
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!receptionStore.current) {
|
||||||
|
const created = await receptionStore.createReception({
|
||||||
|
currentStep: 1,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
if (created) {
|
||||||
|
await router.push(`/reception/${created.id}`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousTypeCode = receptionStore.current.receptionType?.code ?? null
|
||||||
|
const nextTypeCode = selectedReceptionType.value?.code ?? null
|
||||||
|
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousTypeCode === RECEPTION_TYPE_CODES.BOVINS &&
|
||||||
|
nextTypeCode === RECEPTION_TYPE_CODES.MERCHANDISES
|
||||||
|
) {
|
||||||
|
await clearReceptionBovines(receptionIri)
|
||||||
|
}
|
||||||
|
const nextStep = receptionStore.current.currentStep + 1
|
||||||
|
await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
288
frontend/components/reception/reception-product-received.vue
Normal file
288
frontend/components/reception/reception-product-received.vue
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="['flex flex-col items-center gap-16', { submitted }]" @submit.prevent="goNext">
|
||||||
|
<div
|
||||||
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
||||||
|
class="flex flex-col gap-16 items-center w-full">
|
||||||
|
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des marchandises réceptionnnées</h1>
|
||||||
|
<UiSelect
|
||||||
|
id="merchandise-type"
|
||||||
|
v-model="selectedMerchandiseTypeId"
|
||||||
|
label="Type de marchandises"
|
||||||
|
:options="merchandiseTypes.map((type) => ({ value: String(type.id), label: type.label }))"
|
||||||
|
wrapper-class="w-[550px]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && isAutres"
|
||||||
|
class="flex flex-col w-full max-w-[550px]"
|
||||||
|
>
|
||||||
|
<UiTextInput
|
||||||
|
id="merchandise-detail"
|
||||||
|
v-model="merchandiseDetail"
|
||||||
|
label="Préciser"
|
||||||
|
placeholder="Précisions complémentaires"
|
||||||
|
:maxlength="255"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
|
class="flex flex-col gap-4 w-[550px]"
|
||||||
|
>
|
||||||
|
<div class="flex gap-4 justify-between">
|
||||||
|
<div
|
||||||
|
v-for="building in buildings"
|
||||||
|
:key="building.id"
|
||||||
|
>
|
||||||
|
<UiCheckbox
|
||||||
|
v-model="selectedBuildingIds"
|
||||||
|
:value="String(building.id)"
|
||||||
|
:label="building.label"
|
||||||
|
label-class="text-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-red-500 text-sm" :class="showBuildingError ? '' : 'invisible'">
|
||||||
|
Veuillez sélectionner au moins un bâtiment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && isGranule"
|
||||||
|
class="flex flex-col gap-10 w-full max-w-[1100px]"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
||||||
|
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||||
|
<p class="font-bold uppercase text-primary-500">{{ type.label }}</p>
|
||||||
|
<div
|
||||||
|
v-for="building in buildings"
|
||||||
|
:key="building.id"
|
||||||
|
class="flex items-center gap-2 text-lg pl-[2px]"
|
||||||
|
>
|
||||||
|
<UiCheckbox
|
||||||
|
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||||
|
:value="String(building.id)"
|
||||||
|
:label="building.label"
|
||||||
|
label-class="text-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-red-500 text-sm" :class="showBuildingError ? '' : 'invisible'">
|
||||||
|
Veuillez sélectionner au moins un bâtiment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, ref} from 'vue'
|
||||||
|
import {getBuildingList} from '~/services/building'
|
||||||
|
import {getMerchandiseTypeList} from '~/services/merchandise-type'
|
||||||
|
import type {MerchandiseTypeData} from '~/services/dto/merchandise-type-data'
|
||||||
|
import type {BuildingData} from '~/services/dto/building-data'
|
||||||
|
import type {PelletTypeData} from '~/services/dto/pellet-type-data'
|
||||||
|
import {getPelletTypeList} from '~/services/pellet-type'
|
||||||
|
import {
|
||||||
|
createReceptionPelletBuilding,
|
||||||
|
deleteReceptionPelletBuilding,
|
||||||
|
getReceptionPelletBuildingList
|
||||||
|
} from '~/services/reception-pellet-building'
|
||||||
|
import {useReceptionStore} from '~/stores/reception'
|
||||||
|
import {MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES} from '~/utils/constants'
|
||||||
|
import ReceptionBovineReceived from "~/components/reception/reception-bovine-received.vue";
|
||||||
|
|
||||||
|
const receptionStore = useReceptionStore()
|
||||||
|
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
||||||
|
const buildings = ref<BuildingData[]>([])
|
||||||
|
const pelletTypes = ref<PelletTypeData[]>([])
|
||||||
|
const selectedMerchandiseTypeId = ref('')
|
||||||
|
const selectedBuildingIds = ref<string[]>([])
|
||||||
|
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||||
|
const merchandiseDetail = ref('')
|
||||||
|
const submitted = ref(false)
|
||||||
|
const showBuildingError = ref(false)
|
||||||
|
const showPelletBuildingError = ref(false)
|
||||||
|
|
||||||
|
// Extrait l'ID d'une relation depuis un IRI ou un objet complet.
|
||||||
|
const getRelationId = (value: unknown): string | null => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const match = value.match(/\/(\d+)$/)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && 'id' in value) {
|
||||||
|
const record = value as { id?: number | string }
|
||||||
|
if (typeof record.id === 'number') {
|
||||||
|
return String(record.id)
|
||||||
|
}
|
||||||
|
if (typeof record.id === 'string') {
|
||||||
|
return record.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type de marchandise sélectionné dans le select
|
||||||
|
const selectedMerchandiseType = computed(() =>
|
||||||
|
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value)
|
||||||
|
)
|
||||||
|
// Indique si le type est "Granulé"
|
||||||
|
const isGranule = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE)
|
||||||
|
// Indique si le type est "Autres"
|
||||||
|
const isAutres = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES)
|
||||||
|
|
||||||
|
// Charge les référentiels et hydrate le formulaire depuis la réception
|
||||||
|
onMounted(async () => {
|
||||||
|
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
||||||
|
getMerchandiseTypeList(),
|
||||||
|
getBuildingList(),
|
||||||
|
getPelletTypeList()
|
||||||
|
])
|
||||||
|
merchandiseTypes.value = merchandiseTypeList
|
||||||
|
buildings.value = buildingList
|
||||||
|
pelletTypes.value = pelletTypeList
|
||||||
|
|
||||||
|
const currentId = receptionStore.current?.merchandiseType?.id
|
||||||
|
if (currentId) {
|
||||||
|
selectedMerchandiseTypeId.value = String(currentId)
|
||||||
|
}
|
||||||
|
merchandiseDetail.value = receptionStore.current?.merchandiseDetail ?? ''
|
||||||
|
|
||||||
|
selectedBuildingIds.value =
|
||||||
|
receptionStore.current?.buildings?.map((building) => String(building.id)) ?? []
|
||||||
|
|
||||||
|
const existingPelletSelections = receptionStore.current?.pelletBuildings ?? []
|
||||||
|
const selectionMap: Record<string, string[]> = {}
|
||||||
|
for (const selection of existingPelletSelections) {
|
||||||
|
// L'API peut renvoyer les relations comme IRI ou comme objets selon le contexte.
|
||||||
|
const pelletTypeId = getRelationId(selection.pelletType)
|
||||||
|
const buildingId = getRelationId(selection.building)
|
||||||
|
if (!pelletTypeId || !buildingId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!selectionMap[pelletTypeId]) {
|
||||||
|
selectionMap[pelletTypeId] = []
|
||||||
|
}
|
||||||
|
selectionMap[pelletTypeId].push(buildingId)
|
||||||
|
}
|
||||||
|
for (const pelletType of pelletTypes.value) {
|
||||||
|
const key = String(pelletType.id)
|
||||||
|
if (!selectionMap[key]) {
|
||||||
|
selectionMap[key] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedPelletBuildingIds.value = selectionMap
|
||||||
|
})
|
||||||
|
// Enregistre les sélections et passe à l'étape suivante
|
||||||
|
async function goNext() {
|
||||||
|
if (!receptionStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showBuildingError.value = false
|
||||||
|
showPelletBuildingError.value = false
|
||||||
|
|
||||||
|
if (!isGranule.value && !isAutres.value && selectedBuildingIds.value.length === 0) {
|
||||||
|
showBuildingError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGranule.value) {
|
||||||
|
const hasAnyPelletBuilding = Object.values(selectedPelletBuildingIds.value)
|
||||||
|
.some((ids) => ids.length > 0)
|
||||||
|
if (!hasAnyPelletBuilding) {
|
||||||
|
showPelletBuildingError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = receptionStore.current.currentStep + 1
|
||||||
|
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
||||||
|
|
||||||
|
await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
|
merchandiseType: selectedMerchandiseTypeId.value
|
||||||
|
? `/api/merchandise_types/${selectedMerchandiseTypeId.value}`
|
||||||
|
: null,
|
||||||
|
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() : null,
|
||||||
|
buildings: isGranule.value
|
||||||
|
? []
|
||||||
|
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
|
||||||
|
bovineDetail: null,
|
||||||
|
bovinesTypes: null,
|
||||||
|
currentStep: nextStep
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isGranule.value) {
|
||||||
|
await syncPelletSelections(receptionIri)
|
||||||
|
} else {
|
||||||
|
await clearPelletSelections(receptionIri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime toutes les associations granulés/bâtiments existantes
|
||||||
|
async function clearPelletSelections(receptionIri: string) {
|
||||||
|
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||||
|
for (const selection of existing) {
|
||||||
|
await deleteReceptionPelletBuilding(selection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Synchronise les associations granulés/bâtiments avec l'état du formulaire
|
||||||
|
async function syncPelletSelections(receptionIri: string) {
|
||||||
|
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||||
|
const existingMap = new Map<string, number>()
|
||||||
|
for (const selection of existing) {
|
||||||
|
// Construit la table de correspondance avec des IDs normalisés pour éviter les doublons.
|
||||||
|
const pelletTypeId = getRelationId(selection.pelletType)
|
||||||
|
const buildingId = getRelationId(selection.building)
|
||||||
|
if (!pelletTypeId || !buildingId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const key = `${pelletTypeId}:${buildingId}`
|
||||||
|
existingMap.set(key, selection.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
|
||||||
|
for (const [pelletTypeId, buildingIds] of Object.entries(selectedPelletBuildingIds.value)) {
|
||||||
|
for (const buildingId of buildingIds) {
|
||||||
|
desiredEntries.push({pelletTypeId, buildingId})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredKeys = new Set(desiredEntries.map(
|
||||||
|
(entry) => `${entry.pelletTypeId}:${entry.buildingId}`
|
||||||
|
))
|
||||||
|
|
||||||
|
for (const [key, id] of existingMap.entries()) {
|
||||||
|
if (!desiredKeys.has(key)) {
|
||||||
|
await deleteReceptionPelletBuilding(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of desiredEntries) {
|
||||||
|
const key = `${entry.pelletTypeId}:${entry.buildingId}`
|
||||||
|
if (!existingMap.has(key)) {
|
||||||
|
await createReceptionPelletBuilding({
|
||||||
|
reception: receptionIri,
|
||||||
|
pelletType: `/api/pellet_types/${entry.pelletTypeId}`,
|
||||||
|
building: `/api/buildings/${entry.buildingId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
161
frontend/components/reception/update-bovin.vue
Normal file
161
frontend/components/reception/update-bovin.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="flex flex-row justify-between gap-x-12 font-bold uppercase mb-8">
|
||||||
|
<div
|
||||||
|
v-for="type in bovineTypes"
|
||||||
|
:key="type.id"
|
||||||
|
>
|
||||||
|
<UiNumberInput
|
||||||
|
:label="type.label"
|
||||||
|
:code="type.code"
|
||||||
|
v-model="localQuantities[String(type.id)]"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
wrapperClass="w-44 flex-col"
|
||||||
|
inputClass="font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UiNumberInput
|
||||||
|
label="Autres"
|
||||||
|
v-model="localOtherQuantity"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
wrapperClass="w-44 flex-col"
|
||||||
|
inputClass="font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { getBovineTypeList } from '~/services/bovine-type'
|
||||||
|
import type { BovineTypeData } from '~/services/dto/bovine-type-data'
|
||||||
|
import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: ReceptionBovineTypeData[]
|
||||||
|
otherQuantity: number | null
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: ReceptionBovineTypeData[]): void
|
||||||
|
(event: 'update:otherQuantity', value: number | null): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const bovineTypes = ref<BovineTypeData[]>([])
|
||||||
|
const localQuantities = reactive<Record<string, number | null>>({})
|
||||||
|
const localOtherQuantity = ref<number | null>(props.otherQuantity ?? 0)
|
||||||
|
// Verrou pour éviter les boucles props -> local -> emit -> props.
|
||||||
|
const isSyncing = ref(false)
|
||||||
|
|
||||||
|
function entriesEqualByTypeAndQuantity(
|
||||||
|
left: ReceptionBovineTypeData[],
|
||||||
|
right: ReceptionBovineTypeData[]
|
||||||
|
): boolean {
|
||||||
|
const toMap = (entries: ReceptionBovineTypeData[]) => {
|
||||||
|
const map = new Map<number, number>()
|
||||||
|
for (const entry of entries) {
|
||||||
|
const typeId = entry.bovineType?.id ?? 0
|
||||||
|
map.set(typeId, entry.quantity ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = toMap(left)
|
||||||
|
const b = toMap(right)
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [typeId, quantity] of a.entries()) {
|
||||||
|
if ((b.get(typeId) ?? 0) !== quantity) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEntriesFromLocal(): ReceptionBovineTypeData[] {
|
||||||
|
return bovineTypes.value.map((type) => {
|
||||||
|
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
|
||||||
|
return {
|
||||||
|
id: existing?.id ?? 0,
|
||||||
|
bovineType: type,
|
||||||
|
quantity: localQuantities[String(type.id)] ?? 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLocalFromProps() {
|
||||||
|
isSyncing.value = true
|
||||||
|
try {
|
||||||
|
for (const key of Object.keys(localQuantities)) {
|
||||||
|
delete localQuantities[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const type of bovineTypes.value) {
|
||||||
|
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
|
||||||
|
localQuantities[String(type.id)] = existing?.quantity ?? 0
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isSyncing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.otherQuantity,
|
||||||
|
(value) => {
|
||||||
|
if (isSyncing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = value ?? 0
|
||||||
|
isSyncing.value = true
|
||||||
|
localOtherQuantity.value = next
|
||||||
|
isSyncing.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localOtherQuantity, (value) => {
|
||||||
|
if (isSyncing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = value ?? 0
|
||||||
|
emit('update:otherQuantity', next)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {
|
||||||
|
// Hydratation locale uniquement quand le parent change.
|
||||||
|
syncLocalFromProps()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
localQuantities,
|
||||||
|
() => {
|
||||||
|
if (isSyncing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// N'émet que si les quantités diffèrent réellement du parent.
|
||||||
|
const nextEntries = buildEntriesFromLocal()
|
||||||
|
if (!entriesEqualByTypeAndQuantity(nextEntries, props.modelValue)) {
|
||||||
|
emit('update:modelValue', nextEntries)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
bovineTypes.value = await getBovineTypeList()
|
||||||
|
syncLocalFromProps()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
265
frontend/components/reception/update-merchandise.vue
Normal file
265
frontend/components/reception/update-merchandise.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-full relative grid grid-cols-[1fr_200px]">
|
||||||
|
<UiRadioGroup
|
||||||
|
id="merchandise-type"
|
||||||
|
v-model="selectedMerchandiseTypeId"
|
||||||
|
label="Type de marchandises"
|
||||||
|
:options="merchandiseTypes.map((type) => ({
|
||||||
|
value: String(type.id),
|
||||||
|
label: type.label
|
||||||
|
}))"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
option-label-class="uppercase"
|
||||||
|
wrapper-class="w-full uppercase"
|
||||||
|
group-class="grid grid-cols-4 mt-9 mb-7"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
/>
|
||||||
|
<UiTextInput
|
||||||
|
v-if="isAutres"
|
||||||
|
id="merchandise-detail"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
v-model="merchandiseDetail"
|
||||||
|
placeholder="Préciser"
|
||||||
|
:maxlength="255"
|
||||||
|
wrapper-class="w-[200px] mt-12 mb-7"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
|
class="w-full grid grid-cols-[1fr_200px]"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="building in buildings"
|
||||||
|
:key="building.id"
|
||||||
|
>
|
||||||
|
<UiCheckbox
|
||||||
|
v-model="selectedBuildingIds"
|
||||||
|
:value="String(building.id)"
|
||||||
|
:label="building.label"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
label-class="uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && isGranule"
|
||||||
|
class="grid grid-cols-[1fr_200px] w-full col-start-2 row-start-1"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-4 gap-6 justify-between">
|
||||||
|
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||||
|
<p class="mb-1 font-medium uppercase">{{ type.label }}</p>
|
||||||
|
<div
|
||||||
|
v-for="building in buildings"
|
||||||
|
:key="building.id"
|
||||||
|
class="flex text-lg"
|
||||||
|
>
|
||||||
|
<UiCheckbox
|
||||||
|
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||||
|
:value="String(building.id)"
|
||||||
|
:label="building.label"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
label-class="text-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import type { BuildingData } from '~/services/dto/building-data'
|
||||||
|
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||||
|
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
||||||
|
import type { MerchandiseEntryData } from '~/services/dto/reception-data'
|
||||||
|
import { getBuildingList } from '~/services/building'
|
||||||
|
import { getMerchandiseTypeList } from '~/services/merchandise-type'
|
||||||
|
import { getPelletTypeList } from '~/services/pellet-type'
|
||||||
|
import { MERCHANDISE_TYPE_CODES } from '~/utils/constants'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: MerchandiseEntryData
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: MerchandiseEntryData): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
||||||
|
const buildings = ref<BuildingData[]>([])
|
||||||
|
const pelletTypes = ref<PelletTypeData[]>([])
|
||||||
|
|
||||||
|
const selectedMerchandiseTypeId = ref('')
|
||||||
|
const selectedBuildingIds = ref<string[]>([])
|
||||||
|
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||||
|
const merchandiseDetail = ref('')
|
||||||
|
// Verrou de synchro pour empêcher les aller-retours infinis entre parent et composant.
|
||||||
|
const isSyncing = ref(false)
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const selectedMerchandiseType = computed(() =>
|
||||||
|
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value) ?? null
|
||||||
|
)
|
||||||
|
const isGranule = computed(
|
||||||
|
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE
|
||||||
|
)
|
||||||
|
const isAutres = computed(
|
||||||
|
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES
|
||||||
|
)
|
||||||
|
|
||||||
|
function clonePelletSelections(value: Record<string, string[]>) {
|
||||||
|
const clone: Record<string, string[]> = {}
|
||||||
|
for (const [key, buildingIds] of Object.entries(value)) {
|
||||||
|
clone[key] = [...buildingIds]
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
function sorted(values: string[]): string[] {
|
||||||
|
return [...values].sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeModel(value: MerchandiseEntryData): MerchandiseEntryData {
|
||||||
|
// Normalisation stable pour comparer deux modèles sans faux positifs (ordre des tableaux).
|
||||||
|
const pellet: Record<string, string[]> = {}
|
||||||
|
const pelletKeys = Object.keys(value.selectedPelletBuildingIds ?? {}).sort()
|
||||||
|
for (const key of pelletKeys) {
|
||||||
|
pellet[key] = sorted(value.selectedPelletBuildingIds[key] ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
merchandiseTypeId: value.merchandiseTypeId ?? '',
|
||||||
|
merchandiseDetail: value.merchandiseDetail ?? '',
|
||||||
|
selectedBuildingIds: sorted(value.selectedBuildingIds ?? []),
|
||||||
|
selectedPelletBuildingIds: pellet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCurrentModel(): MerchandiseEntryData {
|
||||||
|
return {
|
||||||
|
merchandiseTypeId: selectedMerchandiseTypeId.value,
|
||||||
|
merchandiseDetail: merchandiseDetail.value,
|
||||||
|
selectedBuildingIds: [...selectedBuildingIds.value],
|
||||||
|
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameModel(left: MerchandiseEntryData, right: MerchandiseEntryData): boolean {
|
||||||
|
return JSON.stringify(normalizeModel(left)) === JSON.stringify(normalizeModel(right))
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePelletKeys() {
|
||||||
|
for (const pelletType of pelletTypes.value) {
|
||||||
|
const key = String(pelletType.id)
|
||||||
|
if (!selectedPelletBuildingIds.value[key]) {
|
||||||
|
selectedPelletBuildingIds.value[key] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateFromModelValue(value: MerchandiseEntryData) {
|
||||||
|
isSyncing.value = true
|
||||||
|
try {
|
||||||
|
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
|
||||||
|
merchandiseDetail.value = value.merchandiseDetail ?? ''
|
||||||
|
selectedBuildingIds.value = [...(value.selectedBuildingIds ?? [])]
|
||||||
|
selectedPelletBuildingIds.value = clonePelletSelections(
|
||||||
|
value.selectedPelletBuildingIds ?? {}
|
||||||
|
)
|
||||||
|
ensurePelletKeys()
|
||||||
|
} finally {
|
||||||
|
isSyncing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLocalState() {
|
||||||
|
if (isGranule.value) {
|
||||||
|
if (selectedBuildingIds.value.length > 0) {
|
||||||
|
selectedBuildingIds.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const key of Object.keys(selectedPelletBuildingIds.value)) {
|
||||||
|
if (selectedPelletBuildingIds.value[key].length > 0) {
|
||||||
|
selectedPelletBuildingIds.value[key] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAutres.value && merchandiseDetail.value !== '') {
|
||||||
|
merchandiseDetail.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitCurrentModel() {
|
||||||
|
const currentModel = buildCurrentModel()
|
||||||
|
// Ne pas réémettre si rien n'a changé côté métier.
|
||||||
|
if (isSameModel(currentModel, props.modelValue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', currentModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value) => {
|
||||||
|
const currentModel = buildCurrentModel()
|
||||||
|
// Si local == parent, on ignore pour éviter la boucle de réhydratation.
|
||||||
|
if (isSameModel(currentModel, value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hydrateFromModelValue(value)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
|
||||||
|
() => {
|
||||||
|
if (isSyncing.value || !isReady.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeSanitize = buildCurrentModel()
|
||||||
|
isSyncing.value = true
|
||||||
|
// Applique les règles métier (granulé / autres) avant émission.
|
||||||
|
sanitizeLocalState()
|
||||||
|
isSyncing.value = false
|
||||||
|
|
||||||
|
const afterSanitize = buildCurrentModel()
|
||||||
|
// Si la sanitation a modifié l'état, on laisse le watcher repasser proprement.
|
||||||
|
if (!isSameModel(beforeSanitize, afterSanitize)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emitCurrentModel()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
||||||
|
getMerchandiseTypeList(),
|
||||||
|
getBuildingList(),
|
||||||
|
getPelletTypeList()
|
||||||
|
])
|
||||||
|
merchandiseTypes.value = merchandiseTypeList
|
||||||
|
buildings.value = buildingList
|
||||||
|
pelletTypes.value = pelletTypeList
|
||||||
|
|
||||||
|
hydrateFromModelValue(props.modelValue)
|
||||||
|
isReady.value = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
311
frontend/components/shipment/shipment-form.vue
Normal file
311
frontend/components/shipment/shipment-form.vue
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
||||||
|
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Expédition</h1>
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-user"
|
||||||
|
v-model="form.userId"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:options="users.map((user) => ({
|
||||||
|
value: String(user.id),
|
||||||
|
label: user.username
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingUsers"
|
||||||
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiDateInput
|
||||||
|
id="shipment-date"
|
||||||
|
v-model="form.shipmentDate"
|
||||||
|
label="Date du jour"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="col-start-1 row-start-4 h-[64px]">
|
||||||
|
<div class="flex w-full items-end gap-[104px]">
|
||||||
|
<UiRadioGroup
|
||||||
|
id="shipment-type"
|
||||||
|
name="shipment-type"
|
||||||
|
label="Type d'expédition bovine"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
wrapper-class=""
|
||||||
|
group-class="flex flex-row gap-[104px] w-[160px_160px] h-[32px]"
|
||||||
|
v-model="selectedShipmentTypeId"
|
||||||
|
:options="bovineShipment.map((type) => ({
|
||||||
|
value: String(type.id),
|
||||||
|
label: type.label
|
||||||
|
}))"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiNumberInput
|
||||||
|
id="shipment-type-quantity"
|
||||||
|
v-model="shipmentQuantity"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="1200"
|
||||||
|
:disabled="!selectedShipmentTypeId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-customer"
|
||||||
|
v-model="form.customerId"
|
||||||
|
label="Client"
|
||||||
|
:options="customers.map((customer) => ({
|
||||||
|
value: String(customer.id),
|
||||||
|
label: customer.name || `Client #${customer.id}`
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingCustomers"
|
||||||
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
:options="addressOptions"
|
||||||
|
:disabled="isLoadingCustomers || ownerAddresses.length === 0"
|
||||||
|
label="Adresse"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-truck"
|
||||||
|
v-model="form.truckId"
|
||||||
|
label="Camion"
|
||||||
|
:options="trucks.map((truck) => ({
|
||||||
|
value: String(truck.id),
|
||||||
|
label: truck.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingTrucks"
|
||||||
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-carrier"
|
||||||
|
v-model="form.carrierId"
|
||||||
|
label="Transporteur"
|
||||||
|
:options="carriers.map((carrier) => ({
|
||||||
|
value: String(carrier.id),
|
||||||
|
label: carrier.name
|
||||||
|
}))"
|
||||||
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
id="shipment-vehicle"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
wrapper-class="col-start-2 row-start-5"
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useFormDataLoading } from '~/composables/useFormDataLoading'
|
||||||
|
import { useLiotHandling } from '~/composables/useLiotHandling'
|
||||||
|
import { useAddressSync } from '~/composables/useAddressSync'
|
||||||
|
import type { CustomerData } from '~/services/dto/customer-data'
|
||||||
|
import { getCustomerList } from '~/services/customer'
|
||||||
|
import type { ShipmentFormData } from '~/services/dto/shipment-data'
|
||||||
|
import { useShipmentStore } from '~/stores/shipment'
|
||||||
|
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
||||||
|
import { getShipmentTypeList } from '~/services/shipment-type'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
|
const form = reactive<ShipmentFormData>({
|
||||||
|
userId: '',
|
||||||
|
shipmentDate: new Date().toISOString().slice(0, 10),
|
||||||
|
customerId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: '',
|
||||||
|
licensePlate: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const customers = ref<CustomerData[]>([])
|
||||||
|
const isLoadingCustomers = ref(false)
|
||||||
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
|
const selectedShipmentTypeId = ref('')
|
||||||
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
|
|
||||||
|
const { users, trucks, carriers, isLoadingUsers, isLoadingTrucks, isLoadingCarriers, loadCommonData } =
|
||||||
|
useFormDataLoading(form)
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLiotCarrier, filteredDrivers, filteredVehicles,
|
||||||
|
isLoadingDrivers, isLoadingVehicles, allowAnyLicensePlate,
|
||||||
|
loadDrivers, loadVehicles
|
||||||
|
} = useLiotHandling(form, carriers, isHydrating)
|
||||||
|
|
||||||
|
const customerIdRef = computed(() => form.customerId)
|
||||||
|
const { ownerAddresses, addressOptions } = useAddressSync(form, customerIdRef, customers)
|
||||||
|
|
||||||
|
const loadCustomers = async () => {
|
||||||
|
isLoadingCustomers.value = true
|
||||||
|
try {
|
||||||
|
customers.value = await getCustomerList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCustomers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => shipmentStore.current,
|
||||||
|
(shipment) => {
|
||||||
|
isHydrating.value = true
|
||||||
|
form.licensePlate = shipment?.licensePlate ?? ''
|
||||||
|
form.shipmentDate = shipment?.shipmentDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
|
||||||
|
form.userId = shipment?.user?.id ? String(shipment.user.id) : form.userId
|
||||||
|
form.customerId = shipment?.customer?.id ? String(shipment.customer.id) : ''
|
||||||
|
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
|
||||||
|
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
|
||||||
|
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
|
||||||
|
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
|
||||||
|
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
|
||||||
|
selectedShipmentTypeId.value = shipment?.shipmentType?.id ? String(shipment.shipmentType.id) : ''
|
||||||
|
shipmentQuantity.value = shipment?.nbBovinSend ?? 0
|
||||||
|
isHydrating.value = false
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extra watcher for LIOT defaults after hydration
|
||||||
|
watch(
|
||||||
|
() => isHydrating.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value && isLiotCarrier.value) {
|
||||||
|
if (filteredDrivers.value.length === 1 && !form.driverId) {
|
||||||
|
form.driverId = String(filteredDrivers.value[0].id)
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1 && !form.vehicleId) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
bovineShipment.value = await getShipmentTypeList()
|
||||||
|
await loadCustomers()
|
||||||
|
await loadCommonData()
|
||||||
|
await loadVehicles()
|
||||||
|
await loadDrivers()
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildPayload = () => {
|
||||||
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
|
const normalizedShipmentDate = form.shipmentDate.trim()
|
||||||
|
const normalizedCustomerId = form.customerId.trim()
|
||||||
|
const normalizedTruckId = form.truckId.trim()
|
||||||
|
const normalizedCarrierId = form.carrierId.trim()
|
||||||
|
const normalizedDriverId = form.driverId.trim()
|
||||||
|
const normalizedUserId = form.userId.trim()
|
||||||
|
const normalizedAddressId = form.addressId.trim()
|
||||||
|
|
||||||
|
const customerIri = normalizedCustomerId ? `/api/customers/${normalizedCustomerId}` : null
|
||||||
|
const truckIri = normalizedTruckId ? `/api/trucks/${normalizedTruckId}` : null
|
||||||
|
const carrierIri = normalizedCarrierId ? `/api/carriers/${normalizedCarrierId}` : null
|
||||||
|
const userIri = normalizedUserId ? `/api/users/${normalizedUserId}` : null
|
||||||
|
const driverIri = normalizedDriverId ? `/api/drivers/${normalizedDriverId}` : null
|
||||||
|
const addressIri = normalizedAddressId ? `/api/addresses/${normalizedAddressId}` : null
|
||||||
|
const normalizedShipmentTypeId = selectedShipmentTypeId.value.trim()
|
||||||
|
const shipmentTypeIri = normalizedShipmentTypeId ? `/api/shipment_types/${normalizedShipmentTypeId}` : null
|
||||||
|
|
||||||
|
const rawQuantity = Number(shipmentQuantity.value ?? 0)
|
||||||
|
const normalizedQuantity = Number.isFinite(rawQuantity) ? Math.max(0, Math.trunc(rawQuantity)) : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
licensePlate: normalizedLicensePlate,
|
||||||
|
shipmentDate: normalizedShipmentDate,
|
||||||
|
customer: customerIri,
|
||||||
|
truck: truckIri,
|
||||||
|
carrier: carrierIri,
|
||||||
|
driver: driverIri,
|
||||||
|
user: userIri,
|
||||||
|
address: addressIri,
|
||||||
|
shipmentType: shipmentTypeIri,
|
||||||
|
nbBovinSend: normalizedQuantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDraft = async () => {
|
||||||
|
const payload = buildPayload()
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
await shipmentStore.createShipment({
|
||||||
|
currentStep: 0,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: shipmentStore.current.currentStep,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ saveDraft })
|
||||||
|
|
||||||
|
const validate = async () => {
|
||||||
|
const payload = buildPayload()
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
const created = await shipmentStore.createShipment({
|
||||||
|
currentStep: 1,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
if (created) {
|
||||||
|
await shipmentStore.loadShipment(created.id)
|
||||||
|
await router.push(`/shipment/${created.id}`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextStep = shipmentStore.current.currentStep + 1
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
await shipmentStore.loadShipment(shipmentStore.current.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
26
frontend/components/shipment/shipment-loading.vue
Normal file
26
frontend/components/shipment/shipment-loading.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center gap-[150px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase text-primary-500">Chargement des bovins</h1>
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="goNext"
|
||||||
|
>Peser</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useShipmentStore} from "~/stores/shipment";
|
||||||
|
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
|
||||||
|
const goNext = async () => {
|
||||||
|
const nextStep = shipmentStore.current.currentStep + 1
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: nextStep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
39
frontend/components/ui/UiButton.vue
Normal file
39
frontend/components/ui/UiButton.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="'button'"
|
||||||
|
:type="type"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
class="inline-flex min-w-[194px] items-center justify-center rounded-md"
|
||||||
|
:class="[
|
||||||
|
isDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
|
||||||
|
buttonClass
|
||||||
|
]"
|
||||||
|
v-bind="attrs"
|
||||||
|
>
|
||||||
|
<slot v-if="!loading" />
|
||||||
|
<UiLoadingDots v-else />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, useAttrs} from 'vue'
|
||||||
|
|
||||||
|
defineOptions({inheritAttrs: false})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
type?: 'button' | 'submit' | 'reset'
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
buttonClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
buttonClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const isDisabled = computed(() => props.disabled || props.loading)
|
||||||
|
</script>
|
||||||
76
frontend/components/ui/UiCheckbox.vue
Normal file
76
frontend/components/ui/UiCheckbox.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="wrapperClass">
|
||||||
|
<label
|
||||||
|
class="flex items-center gap-2 cursor-pointer text-primary-700"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="checked"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="['h-4 w-4 cursor-pointer text-primary-500', inputClass]"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
|
<span v-if="label">{{ label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
type CheckboxValue = string | number
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean | CheckboxValue[]
|
||||||
|
value?: CheckboxValue
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
inputClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
value: undefined,
|
||||||
|
label: '',
|
||||||
|
disabled: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
inputClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: boolean | CheckboxValue[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const checked = computed(() => {
|
||||||
|
if (Array.isArray(props.modelValue)) {
|
||||||
|
if (props.value === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return props.modelValue.includes(props.value)
|
||||||
|
}
|
||||||
|
return Boolean(props.modelValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
if (Array.isArray(props.modelValue)) {
|
||||||
|
if (props.value === undefined) {
|
||||||
|
emit('update:modelValue', props.modelValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = new Set(props.modelValue)
|
||||||
|
if (target.checked) {
|
||||||
|
next.add(props.value)
|
||||||
|
} else {
|
||||||
|
next.delete(props.value)
|
||||||
|
}
|
||||||
|
emit('update:modelValue', Array.from(next))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('update:modelValue', target.checked)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
62
frontend/components/ui/UiDateInput.vue
Normal file
62
frontend/components/ui/UiDateInput.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['flex flex-col', wrapperClass]">
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
:for="id"
|
||||||
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
type="date"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
||||||
|
:class="[
|
||||||
|
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
||||||
|
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
inputClass
|
||||||
|
]"
|
||||||
|
@input="onInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, useAttrs } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
label?: string
|
||||||
|
modelValue: string | null | undefined
|
||||||
|
disabled?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
inputClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
inputClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const isEmpty = computed(() => !props.modelValue)
|
||||||
|
|
||||||
|
const onInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
120
frontend/components/ui/UiNumberInput.vue
Normal file
120
frontend/components/ui/UiNumberInput.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// flex row passer en class wraper class flex col ainsi que le wfull 34
|
||||||
|
<template>
|
||||||
|
<div :class="['flex', wrapperClass]">
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
:for="id"
|
||||||
|
class="text-xl flex items-center gap-2 text-primary-700"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="label">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="code"
|
||||||
|
class="text-neutral-600">
|
||||||
|
({{ code }})
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
type="number"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
||||||
|
:class="[
|
||||||
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
|
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||||
|
inputClass
|
||||||
|
]"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
@input="onInput"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, useAttrs} from 'vue'
|
||||||
|
|
||||||
|
defineOptions({inheritAttrs: false})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
label?: string
|
||||||
|
code?: string
|
||||||
|
modelValue: number | string | null | undefined
|
||||||
|
min?: number | string
|
||||||
|
max?: number | string
|
||||||
|
step?: number | string
|
||||||
|
disabled?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
inputClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
min: undefined,
|
||||||
|
max: undefined,
|
||||||
|
step: undefined,
|
||||||
|
disabled: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
inputClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: number | null): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const isEmpty = computed(() => props.modelValue === null || props.modelValue === undefined || props.modelValue === '')
|
||||||
|
|
||||||
|
const toNumberOrNull = (value: number | string | undefined) => {
|
||||||
|
if (value === undefined || value === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
if (target.value === '') {
|
||||||
|
emit('update:modelValue', null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parsed = Number(target.value)
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
emit('update:modelValue', null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = toNumberOrNull(props.min)
|
||||||
|
const max = toNumberOrNull(props.max)
|
||||||
|
|
||||||
|
let numeric = parsed
|
||||||
|
if (min !== null) {
|
||||||
|
numeric = Math.max(min, numeric)
|
||||||
|
} else {
|
||||||
|
numeric = Math.max(0, numeric)
|
||||||
|
}
|
||||||
|
if (max !== null) {
|
||||||
|
numeric = Math.min(max, numeric)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.value = String(numeric)
|
||||||
|
emit('update:modelValue', numeric)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === '-' || event.key === 'e' || event.key === 'E') {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
93
frontend/components/ui/UiRadioGroup.vue
Normal file
93
frontend/components/ui/UiRadioGroup.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['flex flex-col', wrapperClass]">
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
:aria-label="label || id || 'radio-group'"
|
||||||
|
:class="['flex items-center gap-6 mt-1', groupClass]"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-for="option in options"
|
||||||
|
:key="String(option.value)"
|
||||||
|
:for="`${id || 'radio'}-${option.value}`"
|
||||||
|
class="flex items-center gap-2 text-primary-700"
|
||||||
|
:class="itemClass"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`${id || 'radio'}-${option.value}`"
|
||||||
|
type="radio"
|
||||||
|
:name="name || id || 'radio-group'"
|
||||||
|
:value="String(option.value)"
|
||||||
|
:checked="String(modelValue ?? '') === String(option.value)"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="h-4 w-4 border-primary-700/50 text-primary-700 focus:ring-primary-700"
|
||||||
|
:class="[
|
||||||
|
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
inputClass
|
||||||
|
]"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
|
<span class="text-xl" :class="optionLabelClass">
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAttrs } from 'vue'
|
||||||
|
|
||||||
|
type RadioOption = {
|
||||||
|
value: string | number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
label?: string
|
||||||
|
modelValue: string | number | null | undefined
|
||||||
|
options: RadioOption[]
|
||||||
|
disabled?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
groupClass?: string
|
||||||
|
itemClass?: string
|
||||||
|
inputClass?: string
|
||||||
|
optionLabelClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
label: '',
|
||||||
|
disabled: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
groupClass: '',
|
||||||
|
itemClass: '',
|
||||||
|
inputClass: '',
|
||||||
|
optionLabelClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const onChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
85
frontend/components/ui/UiSelect.vue
Normal file
85
frontend/components/ui/UiSelect.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['flex flex-col', wrapperClass]">
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
:for="id"
|
||||||
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
:id="id"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:disabled="disabled || loading"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] bg-transparent"
|
||||||
|
:class="[
|
||||||
|
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
||||||
|
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
selectClass
|
||||||
|
]"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
|
<option value="" disabled class="text-neutral-400">
|
||||||
|
{{ placeholderText }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
class="text-primary-700"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, useAttrs } from 'vue'
|
||||||
|
|
||||||
|
type SelectOption = {
|
||||||
|
value: string | number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
modelValue: string | number | null | undefined
|
||||||
|
options: SelectOption[]
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
selectClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
placeholder: 'Sélectionner',
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
selectClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const isEmpty = computed(() => props.modelValue === '' || props.modelValue === null || props.modelValue === undefined)
|
||||||
|
const placeholderText = computed(() => props.placeholder || 'Sélectionner')
|
||||||
|
|
||||||
|
const onChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLSelectElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
68
frontend/components/ui/UiTextInput.vue
Normal file
68
frontend/components/ui/UiTextInput.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['flex flex-col', wrapperClass]">
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
:for="id"
|
||||||
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
type="text"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="border-b border-black text-xl py-[6px] bg-transparent text-primary-700"
|
||||||
|
:class="[
|
||||||
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
|
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||||
|
inputClass
|
||||||
|
]"
|
||||||
|
@input="onInput"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, useAttrs } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
label?: string
|
||||||
|
modelValue: string | null | undefined
|
||||||
|
placeholder?: string
|
||||||
|
maxlength?: number | string
|
||||||
|
disabled?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
inputClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
placeholder: '',
|
||||||
|
maxlength: undefined,
|
||||||
|
disabled: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
inputClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const isEmpty = computed(() => !props.modelValue)
|
||||||
|
|
||||||
|
const onInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
91
frontend/components/ui/license-plate-input.vue
Normal file
91
frontend/components/ui/license-plate-input.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label :for="inputId" class="font-bold uppercase text-xl text-primary-500">{{ label }}</label>
|
||||||
|
<div class="flex items-end gap-8">
|
||||||
|
<input
|
||||||
|
:id="inputId"
|
||||||
|
:value="modelValue"
|
||||||
|
v-maska="maskOptions"
|
||||||
|
type="text"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
:placeholder="placeholderText"
|
||||||
|
:required="required"
|
||||||
|
class="border-b border-black flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<UiCheckbox
|
||||||
|
:id="checkboxId"
|
||||||
|
:model-value="allowAny"
|
||||||
|
label="Autoriser un format libre"
|
||||||
|
wrapper-class="ml-auto"
|
||||||
|
label-class="gap-3 whitespace-nowrap text-sm"
|
||||||
|
input-class="h-4 w-4 accent-primary-500"
|
||||||
|
@update:modelValue="handleAllowAnyChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { vMaska } from 'maska/vue'
|
||||||
|
type Props = {
|
||||||
|
modelValue: string
|
||||||
|
allowAny?: boolean
|
||||||
|
label?: string
|
||||||
|
id?: string
|
||||||
|
required?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
allowAny: false,
|
||||||
|
label: 'Immatriculation',
|
||||||
|
id: 'license-plate',
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
(event: 'update:allowAny', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const inputId = computed(() => props.id)
|
||||||
|
const checkboxId = computed(() => `${props.id}-format`)
|
||||||
|
|
||||||
|
const maskOptions = computed(() =>
|
||||||
|
props.allowAny
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
mask: '@@-###-@@',
|
||||||
|
eager: true,
|
||||||
|
tokens: {
|
||||||
|
'@': {
|
||||||
|
pattern: /[A-Za-z]/,
|
||||||
|
transform: (char: string) => char.toUpperCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const placeholderText = computed(() => (props.allowAny ? '' : 'AA-123-AA'))
|
||||||
|
const maxLength = computed(() => (props.allowAny ? 20 : 9))
|
||||||
|
|
||||||
|
const handleInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement | null
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.allowAny) {
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAllowAnyChange = (nextValue: boolean) => {
|
||||||
|
emit('update:allowAny', nextValue)
|
||||||
|
if (!nextValue) {
|
||||||
|
emit('update:modelValue', props.modelValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
50
frontend/components/ui/loading-dots.vue
Normal file
50
frontend/components/ui/loading-dots.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-2 text-sm uppercase">
|
||||||
|
<span class="loader-dots">
|
||||||
|
<span class="loader-dot"></span>
|
||||||
|
<span class="loader-dot"></span>
|
||||||
|
<span class="loader-dot"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loader-dots {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-dot {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: currentColor;
|
||||||
|
animation: loader-bounce 1s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-dot:nth-child(2) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-dot:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader-bounce {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0.6);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
frontend/components/ui/stepper.vue
Normal file
84
frontend/components/ui/stepper.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative w-full">
|
||||||
|
<div class="relative h-[18px] text-[16px] uppercase font-bold text-black mb-3">
|
||||||
|
<div
|
||||||
|
v-for="(label, index) in labels"
|
||||||
|
:key="label"
|
||||||
|
class="absolute top-0 whitespace-nowrap text-primary-500"
|
||||||
|
:class="labelClass(index)"
|
||||||
|
:style="positionStyle(index)"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative h-[22px]">
|
||||||
|
<div class="absolute left-0 right-0 top-1/2 h-[2px] -translate-y-1/2 bg-black"></div>
|
||||||
|
<div
|
||||||
|
v-for="(_, index) in labels"
|
||||||
|
:key="index"
|
||||||
|
class="absolute top-1/2 h-[22px] w-[22px] -translate-y-1/2 rounded-full border border-black"
|
||||||
|
:class="[
|
||||||
|
dotClass(index),
|
||||||
|
isActive(index) ? 'bg-black' : 'bg-white',
|
||||||
|
isClickable(index) ? 'cursor-pointer' : 'cursor-not-allowed'
|
||||||
|
]"
|
||||||
|
:style="positionStyle(index)"
|
||||||
|
@click="handleClick(index)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type Props = {
|
||||||
|
labels: string[]
|
||||||
|
currentStep: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', step: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const stepCount = computed(() => Math.max(props.labels.length, 1))
|
||||||
|
|
||||||
|
const positionStyle = (index: number) => {
|
||||||
|
if (stepCount.value <= 1) {
|
||||||
|
return { left: '0%' }
|
||||||
|
}
|
||||||
|
if (index === 0) {
|
||||||
|
return { left: '0%' }
|
||||||
|
}
|
||||||
|
if (index === stepCount.value - 1) {
|
||||||
|
return { right: '0%' }
|
||||||
|
}
|
||||||
|
const percent = (index / (stepCount.value - 1)) * 100
|
||||||
|
return { left: `${percent}%` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMiddle = (index: number) => index > 0 && index < stepCount.value - 1
|
||||||
|
const isLast = (index: number) => index === stepCount.value - 1
|
||||||
|
|
||||||
|
const dotClass = (index: number) => (isMiddle(index) ? '-translate-x-1/2' : '')
|
||||||
|
|
||||||
|
const labelClass = (index: number) => {
|
||||||
|
if (isLast(index)) {
|
||||||
|
return 'text-right'
|
||||||
|
}
|
||||||
|
if (isMiddle(index)) {
|
||||||
|
return 'text-center -translate-x-1/2'
|
||||||
|
}
|
||||||
|
return 'text-left'
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = (index: number) => index === props.currentStep
|
||||||
|
|
||||||
|
const isClickable = (index: number) => index <= props.currentStep
|
||||||
|
|
||||||
|
const handleClick = (index: number) => {
|
||||||
|
if (!isClickable(index)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('select', index)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
57
frontend/components/workflow/workflow-liot-fields.vue
Normal file
57
frontend/components/workflow/workflow-liot-fields.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="!isLiotCarrier">
|
||||||
|
<div :class="wrapperClass">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="isLiotCarrier">
|
||||||
|
<UiSelect
|
||||||
|
:id="`${idPrefix}-vehicle`"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
:wrapper-class="wrapperClass"
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
:id="`${idPrefix}-driver`"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
:wrapper-class="driverWrapperClass"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
idPrefix: string
|
||||||
|
form: { licensePlate: string; vehicleId: string; driverId: string }
|
||||||
|
isLiotCarrier: boolean
|
||||||
|
allowAnyLicensePlate: boolean
|
||||||
|
filteredVehicles: VehicleData[]
|
||||||
|
filteredDrivers: DriverData[]
|
||||||
|
isLoadingVehicles: boolean
|
||||||
|
isLoadingDrivers: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
driverWrapperClass?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
'update:allowAnyLicensePlate': [value: boolean]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
72
frontend/components/workflow/workflow-waiting-list.vue
Normal file
72
frontend/components/workflow/workflow-waiting-list.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
<h1 class="text-3xl font-bold uppercase text-primary-500">{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px]">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16">
|
||||||
|
<div
|
||||||
|
class="grid gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
:style="{ gridTemplateColumns: gridCols }"
|
||||||
|
>
|
||||||
|
<div v-for="col in columns" :key="col.key">{{ col.label }}</div>
|
||||||
|
<div v-if="showActions">Actions</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
class="grid gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
:style="{ gridTemplateColumns: gridCols }"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToItem(item.id)"
|
||||||
|
@keydown.enter="goToItem(item.id)"
|
||||||
|
>
|
||||||
|
<div v-for="col in columns" :key="col.key">
|
||||||
|
<slot :name="`cell-${col.key}`" :item="item">
|
||||||
|
{{ getNestedValue(item, col.key) }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="showActions" @click.stop>
|
||||||
|
<slot name="actions" :item="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Column {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
title: string
|
||||||
|
columns: Column[]
|
||||||
|
items: any[]
|
||||||
|
routePrefix: string
|
||||||
|
showActions?: boolean
|
||||||
|
}>(), {
|
||||||
|
showActions: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const gridCols = computed(() => {
|
||||||
|
const dataCols = props.columns.map(() => '1fr').join(' ')
|
||||||
|
return props.showActions ? `${dataCols} 60px` : dataCols
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToItem = (id: number) => {
|
||||||
|
router.push(`${props.routePrefix}/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNestedValue = (obj: any, path: string): string => {
|
||||||
|
const value = path.split('.').reduce((acc, key) => acc?.[key], obj)
|
||||||
|
return value || '—'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
78
frontend/components/workflow/workflow-weight.vue
Normal file
78
frontend/components/workflow/workflow-weight.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
|
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
||||||
|
<div
|
||||||
|
v-if="!displayWeight"
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<div v-else class="w-full">
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
|
||||||
|
{{ displayWeight }} kg
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-[54px]">
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
@click="fetchWeight"
|
||||||
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="saveWeight"
|
||||||
|
>Valider la pesée</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="printReceipt"
|
||||||
|
>Générer le bon</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRef } from 'vue'
|
||||||
|
import { useWeighingStep } from '~/composables/steps/useWeighingStep'
|
||||||
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
entityName: 'reception' | 'shipment'
|
||||||
|
apiResource: string
|
||||||
|
titleLabel: string
|
||||||
|
isFinal: boolean
|
||||||
|
entity: any
|
||||||
|
getWeightFromScale: () => Promise<WeightData>
|
||||||
|
updateEntity: (id: number, payload: any) => Promise<any>
|
||||||
|
loadEntity: (id: number) => Promise<any>
|
||||||
|
clearEntity: () => void
|
||||||
|
buildReceiptFilename: (entity: any) => string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const entityRef = toRef(props, 'entity')
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayWeight,
|
||||||
|
title,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight,
|
||||||
|
showGenerateReceipt,
|
||||||
|
printReceipt
|
||||||
|
} = useWeighingStep({
|
||||||
|
mode: props.mode,
|
||||||
|
entity: entityRef,
|
||||||
|
entityName: props.entityName,
|
||||||
|
apiResource: props.apiResource,
|
||||||
|
titleLabel: props.titleLabel,
|
||||||
|
isFinal: props.isFinal,
|
||||||
|
getWeightFromScale: props.getWeightFromScale,
|
||||||
|
updateEntity: props.updateEntity,
|
||||||
|
loadEntity: props.loadEntity,
|
||||||
|
clearEntity: props.clearEntity,
|
||||||
|
buildReceiptFilename: props.buildReceiptFilename
|
||||||
|
})
|
||||||
|
</script>
|
||||||
77
frontend/composables/steps/useWeighingStep.ts
Normal file
77
frontend/composables/steps/useWeighingStep.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { useWeighing } from '~/composables/useWeighing'
|
||||||
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
interface UseWeighingStepOptions {
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
entity: Ref<any>
|
||||||
|
entityName: 'reception' | 'shipment'
|
||||||
|
apiResource: string
|
||||||
|
titleLabel: string
|
||||||
|
isFinal: boolean
|
||||||
|
getWeightFromScale: () => Promise<WeightData>
|
||||||
|
updateEntity: (id: number, payload: any) => Promise<any>
|
||||||
|
loadEntity: (id: number) => Promise<any>
|
||||||
|
clearEntity: () => void
|
||||||
|
buildReceiptFilename: (entity: any) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWeighingStep = (options: UseWeighingStepOptions) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
} = useWeighing({
|
||||||
|
mode: options.mode,
|
||||||
|
entity: options.entity,
|
||||||
|
entityName: options.entityName,
|
||||||
|
apiResource: options.apiResource,
|
||||||
|
titleLabel: options.titleLabel,
|
||||||
|
getWeightFromScale: options.getWeightFromScale,
|
||||||
|
updateEntity: options.updateEntity,
|
||||||
|
loadEntity: options.loadEntity
|
||||||
|
})
|
||||||
|
|
||||||
|
const showGenerateReceipt = computed(
|
||||||
|
() => options.isFinal && displayWeight.value !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
const printReceipt = async () => {
|
||||||
|
if (!import.meta.client || !options.entity.value) return
|
||||||
|
|
||||||
|
await saveWeight()
|
||||||
|
const entity = options.entity.value
|
||||||
|
const filename = options.buildReceiptFilename(entity)
|
||||||
|
await printPdf(`/${options.apiResource}/${entity.id}/receipt`, filename)
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
const result = await options.updateEntity(entity.id, { isValid: true })
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
options.clearEntity()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight,
|
||||||
|
showGenerateReceipt,
|
||||||
|
printReceipt
|
||||||
|
}
|
||||||
|
}
|
||||||
54
frontend/composables/useAddressSync.ts
Normal file
54
frontend/composables/useAddressSync.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { AddressData } from '~/services/dto/address-data'
|
||||||
|
|
||||||
|
interface AddressOwner {
|
||||||
|
id: number
|
||||||
|
addresses?: AddressData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAddressSync = (
|
||||||
|
form: { addressId: string },
|
||||||
|
ownerId: Ref<string>,
|
||||||
|
owners: Ref<AddressOwner[]>
|
||||||
|
) => {
|
||||||
|
const ownerAddresses = computed<AddressData[]>(() => {
|
||||||
|
const id = Number(ownerId.value)
|
||||||
|
if (!Number.isFinite(id)) return []
|
||||||
|
return owners.value.find((owner) => owner.id === id)?.addresses ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
const addressOptions = computed(() =>
|
||||||
|
ownerAddresses.value.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [ownerId.value, form.addressId, owners.value],
|
||||||
|
() => {
|
||||||
|
if (!ownerId.value) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && ownerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(ownerAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) return
|
||||||
|
const matches = ownerAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (ownerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(ownerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ownerAddresses, addressOptions }
|
||||||
|
}
|
||||||
183
frontend/composables/useApi.ts
Normal file
183
frontend/composables/useApi.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import type { FetchOptions } from 'ofetch'
|
||||||
|
import { $fetch, FetchError } from 'ofetch'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
export type AnyObject = Record<string, unknown>
|
||||||
|
|
||||||
|
export type ApiClient = {
|
||||||
|
get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<Blob>
|
||||||
|
post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
delete<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
|
||||||
|
FetchOptions<ResponseType> & {
|
||||||
|
toast?: boolean
|
||||||
|
toastTitle?: string
|
||||||
|
toastErrorMessage?: string
|
||||||
|
toastSuccessMessage?: string
|
||||||
|
toastErrorKey?: string
|
||||||
|
toastSuccessKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useApi = (): ApiClient => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const baseURL = config.public.apiBase ?? '/api'
|
||||||
|
const toast = useToast()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
let isHandlingUnauthorized = false
|
||||||
|
const i18n = nuxtApp.$i18n as
|
||||||
|
| {
|
||||||
|
t: (key: string) => string
|
||||||
|
te?: (key: string) => boolean
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
|
||||||
|
const te = (key: string) => (i18n?.te ? i18n.te(key) : false)
|
||||||
|
|
||||||
|
const extractErrorMessage = (error: unknown, responseData?: unknown): string => {
|
||||||
|
const data = responseData ?? (error as FetchError)?.data
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
const record = data as Record<string, unknown>
|
||||||
|
return (
|
||||||
|
(record['hydra:description'] as string) ||
|
||||||
|
(record.detail as string) ||
|
||||||
|
(record.message as string) ||
|
||||||
|
(record.error as string) ||
|
||||||
|
(record.title as string) ||
|
||||||
|
(record['hydra:title'] as string) ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (error as FetchError)?.message ?? 'Erreur inconnue.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodErrorKeys: Record<string, string> = {
|
||||||
|
GET: 'errors.http.get',
|
||||||
|
POST: 'errors.http.post',
|
||||||
|
PUT: 'errors.http.put',
|
||||||
|
PATCH: 'errors.http.patch',
|
||||||
|
DELETE: 'errors.http.delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = $fetch.create({
|
||||||
|
baseURL,
|
||||||
|
retry: 0,
|
||||||
|
credentials: 'include',
|
||||||
|
onResponse({ options, response }) {
|
||||||
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
|
if (apiOptions?.toast === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response?.status && response.status >= 400) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const successKey = apiOptions?.toastSuccessKey
|
||||||
|
const successMessage =
|
||||||
|
apiOptions?.toastSuccessMessage ||
|
||||||
|
(successKey ? (te(successKey) ? t(successKey) : successKey) : '')
|
||||||
|
|
||||||
|
if (successMessage) {
|
||||||
|
toast.success({
|
||||||
|
title: 'Succès',
|
||||||
|
message: successMessage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onResponseError({ response, error, options }) {
|
||||||
|
if (response?.status === 401) {
|
||||||
|
const requestUrl = typeof options?.url === 'string' ? options.url : ''
|
||||||
|
if (!requestUrl.includes('login_check') && !requestUrl.includes('logout')) {
|
||||||
|
if (!isHandlingUnauthorized) {
|
||||||
|
isHandlingUnauthorized = true
|
||||||
|
auth.clearSession()
|
||||||
|
const route = useRoute()
|
||||||
|
if (route.path !== '/login') {
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
isHandlingUnauthorized = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
|
if (apiOptions?.toast === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const method =
|
||||||
|
typeof options?.method === 'string' ? options.method.toUpperCase() : 'GET'
|
||||||
|
const defaultKey = methodErrorKeys[method]
|
||||||
|
const defaultMessage =
|
||||||
|
defaultKey && te(defaultKey) ? t(defaultKey) : ''
|
||||||
|
const errorKey = apiOptions?.toastErrorKey
|
||||||
|
const errorMessage =
|
||||||
|
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
|
||||||
|
const extractedMessage = extractErrorMessage(error, response?._data)
|
||||||
|
const message =
|
||||||
|
apiOptions?.toastErrorMessage ||
|
||||||
|
errorMessage ||
|
||||||
|
defaultMessage ||
|
||||||
|
extractedMessage ||
|
||||||
|
'Une erreur est survenue.'
|
||||||
|
|
||||||
|
toast.error({
|
||||||
|
title: apiOptions?.toastTitle ?? 'Erreur',
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const request = <T>(
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
||||||
|
url: string,
|
||||||
|
options: ApiFetchOptions<'json'> = {}
|
||||||
|
) => {
|
||||||
|
const needsJsonBody = method === 'POST' || method === 'PUT'
|
||||||
|
const needsMergePatch = method === 'PATCH'
|
||||||
|
|
||||||
|
const headers = new Headers(options.headers as HeadersInit | undefined)
|
||||||
|
|
||||||
|
if (needsMergePatch && !headers.has('Content-Type')) {
|
||||||
|
headers.set('Content-Type', 'application/merge-patch+json')
|
||||||
|
} else if (needsJsonBody && !headers.has('Content-Type')) {
|
||||||
|
headers.set('Content-Type', 'application/json')
|
||||||
|
}
|
||||||
|
|
||||||
|
return client<T>(url, { ...options, method, headers })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('GET', url, { ...options, query })
|
||||||
|
},
|
||||||
|
getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
|
||||||
|
return client<Blob>(url, { ...options, method: 'GET', query, responseType: 'blob' })
|
||||||
|
},
|
||||||
|
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('POST', url, { ...options, body })
|
||||||
|
},
|
||||||
|
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('PUT', url, { ...options, body })
|
||||||
|
},
|
||||||
|
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('PATCH', url, { ...options, body })
|
||||||
|
},
|
||||||
|
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
|
return request<T>('DELETE', url, { ...options, query })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
frontend/composables/useAppVersion.ts
Normal file
17
frontend/composables/useAppVersion.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const useAppVersion = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const version = useState<string | null>('app-version', () => null)
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
if (version.value) {
|
||||||
|
return version.value
|
||||||
|
}
|
||||||
|
const response = await api.get<{ version: string }>('version', {}, {
|
||||||
|
toast: false
|
||||||
|
})
|
||||||
|
version.value = response.version
|
||||||
|
return version.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { version, load }
|
||||||
|
}
|
||||||
73
frontend/composables/useFormDataLoading.ts
Normal file
73
frontend/composables/useFormDataLoading.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { TruckData } from '~/services/dto/truck-data'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import { getUsers } from '~/services/auth'
|
||||||
|
import { getTruckList } from '~/services/truck'
|
||||||
|
import { getCarrierList } from '~/services/carrier'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
export const useFormDataLoading = (form: { userId: string }) => {
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTrucks = async () => {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCarriers = async () => {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDefaultUser = () => {
|
||||||
|
if (form.userId) return
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCommonData = async () => {
|
||||||
|
await loadUsers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
trucks,
|
||||||
|
carriers,
|
||||||
|
isLoadingUsers,
|
||||||
|
isLoadingTrucks,
|
||||||
|
isLoadingCarriers,
|
||||||
|
loadUsers,
|
||||||
|
loadTrucks,
|
||||||
|
loadCarriers,
|
||||||
|
setDefaultUser,
|
||||||
|
loadCommonData
|
||||||
|
}
|
||||||
|
}
|
||||||
153
frontend/composables/useLiotHandling.ts
Normal file
153
frontend/composables/useLiotHandling.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
import { getDriverList } from '~/services/driver'
|
||||||
|
import { getVehicleList } from '~/services/vehicle'
|
||||||
|
import { SUPPLIER_CODE } from '~/utils/constants'
|
||||||
|
|
||||||
|
interface LiotForm {
|
||||||
|
carrierId: string
|
||||||
|
truckId: string
|
||||||
|
driverId: string
|
||||||
|
vehicleId: string
|
||||||
|
licensePlate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLiotHandling = (
|
||||||
|
form: LiotForm,
|
||||||
|
carriers: Ref<CarrierData[]>,
|
||||||
|
isHydrating: Ref<boolean>
|
||||||
|
) => {
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) return []
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||||
|
if (!form.carrierId) return []
|
||||||
|
return vehicles.value.filter(
|
||||||
|
(vehicle) =>
|
||||||
|
String(vehicle.carrier?.id) === form.carrierId &&
|
||||||
|
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-select driver/vehicle when carrier changes
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value) return
|
||||||
|
if (!form.carrierId) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredDrivers.value.length === 1) {
|
||||||
|
form.driverId = String(filteredDrivers.value[0].id)
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate/auto-select vehicle when truck/carrier changes
|
||||||
|
watch(
|
||||||
|
() => [form.truckId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) return
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.vehicleId) return
|
||||||
|
const matches = filteredVehicles.value.some(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
form.vehicleId = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sync license plate from selected vehicle
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) return
|
||||||
|
if (isHydrating.value) return
|
||||||
|
const selected = filteredVehicles.value.find(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
form.licensePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-select vehicle from license plate
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value || form.vehicleId) return
|
||||||
|
const match = filteredVehicles.value.find(
|
||||||
|
(vehicle) => vehicle.plate === form.licensePlate
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
form.vehicleId = String(match.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
drivers,
|
||||||
|
vehicles,
|
||||||
|
isLoadingDrivers,
|
||||||
|
isLoadingVehicles,
|
||||||
|
allowAnyLicensePlate,
|
||||||
|
isLiotCarrier,
|
||||||
|
filteredDrivers,
|
||||||
|
filteredVehicles,
|
||||||
|
loadDrivers,
|
||||||
|
loadVehicles
|
||||||
|
}
|
||||||
|
}
|
||||||
29
frontend/composables/usePdfPrinter.ts
Normal file
29
frontend/composables/usePdfPrinter.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
|
||||||
|
export const usePdfPrinter = () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const printPdf = async (url: string, filename = 'document.pdf'): Promise<void> => {
|
||||||
|
const blob = await api.getBlob(url)
|
||||||
|
|
||||||
|
const pdfBlob = blob.type === 'application/pdf'
|
||||||
|
? blob
|
||||||
|
: new Blob([blob], { type: 'application/pdf' })
|
||||||
|
|
||||||
|
const blobUrl = URL.createObjectURL(pdfBlob)
|
||||||
|
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = blobUrl
|
||||||
|
a.download = filename
|
||||||
|
a.style.display = 'none'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
a.remove()
|
||||||
|
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
|
||||||
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
printPdf
|
||||||
|
}
|
||||||
|
}
|
||||||
130
frontend/composables/useWeighing.ts
Normal file
130
frontend/composables/useWeighing.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { WeightEntryData } from '~/services/dto/reception-data'
|
||||||
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
import { createWeight, updateWeight } from '~/services/weight'
|
||||||
|
|
||||||
|
export type WeighingMode = 'gross' | 'tare'
|
||||||
|
|
||||||
|
export interface UseWeighingOptions {
|
||||||
|
mode: WeighingMode
|
||||||
|
entity: Ref<{ id: number; currentStep: number; isValid: boolean; weights?: WeightEntryData[] | null } | null>
|
||||||
|
entityName: 'reception' | 'shipment'
|
||||||
|
apiResource: string
|
||||||
|
titleLabel: string
|
||||||
|
getWeightFromScale: () => Promise<WeightData>
|
||||||
|
updateEntity: (id: number, payload: any) => Promise<any>
|
||||||
|
loadEntity?: (id: number) => Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWeighing = ({
|
||||||
|
mode,
|
||||||
|
entity,
|
||||||
|
entityName,
|
||||||
|
apiResource,
|
||||||
|
titleLabel,
|
||||||
|
getWeightFromScale,
|
||||||
|
updateEntity,
|
||||||
|
loadEntity
|
||||||
|
}: UseWeighingOptions) => {
|
||||||
|
const weightData = ref<WeightData | null>(null)
|
||||||
|
const isFetching = ref(false)
|
||||||
|
|
||||||
|
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
||||||
|
const weights = entity.value?.weights ?? []
|
||||||
|
return weights.find((entry) => entry.type === mode) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
|
const title = computed(() => titleLabel)
|
||||||
|
const showLoadingBox = computed(() => isFetching.value)
|
||||||
|
|
||||||
|
const fetchWeight = async () => {
|
||||||
|
isFetching.value = true
|
||||||
|
weightData.value = await getWeightFromScale().finally(() => {
|
||||||
|
isFetching.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveWeight = async () => {
|
||||||
|
if (!entity.value) return
|
||||||
|
|
||||||
|
const existingEntry = currentWeightEntry.value
|
||||||
|
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
||||||
|
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
||||||
|
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||||
|
|
||||||
|
if (baseWeight === null) return
|
||||||
|
|
||||||
|
const relationPayload: Record<string, string> = {}
|
||||||
|
relationPayload[entityName] = `/api/${apiResource}/${entity.value.id}`
|
||||||
|
|
||||||
|
if (existingEntry?.id) {
|
||||||
|
await updateWeight(existingEntry.id, {
|
||||||
|
type: mode,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createWeight({
|
||||||
|
...relationPayload,
|
||||||
|
type: mode,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = mode === 'tare'
|
||||||
|
? entity.value.currentStep
|
||||||
|
: entity.value.currentStep + 1
|
||||||
|
await updateEntity(entity.value.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
isValid: entity.value.isValid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loadEntity) {
|
||||||
|
await loadEntity(entity.value.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compatible aliases
|
||||||
|
export const useWeighingShipment = ({
|
||||||
|
modeShipment,
|
||||||
|
shipment,
|
||||||
|
updateShipment,
|
||||||
|
loadShipment
|
||||||
|
}: {
|
||||||
|
modeShipment: WeighingMode
|
||||||
|
shipment: Ref<any>
|
||||||
|
updateShipment: (id: number, payload: any) => Promise<any>
|
||||||
|
loadShipment?: (id: number) => Promise<any>
|
||||||
|
}) => {
|
||||||
|
return useWeighing({
|
||||||
|
mode: modeShipment,
|
||||||
|
entity: shipment,
|
||||||
|
entityName: 'shipment',
|
||||||
|
apiResource: 'shipments',
|
||||||
|
titleLabel: modeShipment === 'gross' ? 'Pesée à vide' : 'Pesée à plein',
|
||||||
|
getWeightFromScale: async () => {
|
||||||
|
const { getWeightShipment } = await import('~/services/shipment')
|
||||||
|
return getWeightShipment()
|
||||||
|
},
|
||||||
|
updateEntity: updateShipment,
|
||||||
|
loadEntity: loadShipment
|
||||||
|
})
|
||||||
|
}
|
||||||
83
frontend/composables/useWorkflowSteps.ts
Normal file
83
frontend/composables/useWorkflowSteps.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { WorkflowConfig } from '~/types/workflow'
|
||||||
|
|
||||||
|
interface WorkflowStore {
|
||||||
|
current: any
|
||||||
|
isLoading: boolean
|
||||||
|
clearCurrent: () => void
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWorkflowSteps = (config: WorkflowConfig, store: WorkflowStore) => {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const stepLabels = config.steps.map(s => s.label)
|
||||||
|
|
||||||
|
const currentStep = computed(() => store.current?.currentStep ?? 0)
|
||||||
|
const entity = computed(() => store.current)
|
||||||
|
|
||||||
|
const loadMethod = `load${config.entityName.charAt(0).toUpperCase() + config.entityName.slice(1)}`
|
||||||
|
const updateMethod = `update${config.entityName.charAt(0).toUpperCase() + config.entityName.slice(1)}`
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
async (param) => {
|
||||||
|
const id = resolveId(param)
|
||||||
|
if (id === null) {
|
||||||
|
store.clearCurrent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await store[loadMethod](id)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveAndHold = async () => {
|
||||||
|
if (!store.current) {
|
||||||
|
await router.push('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const datePayload: Record<string, any> = {}
|
||||||
|
datePayload[config.dateField] = store.current[config.dateField]
|
||||||
|
await store[updateMethod](store.current.id, {
|
||||||
|
currentStep: store.current.currentStep,
|
||||||
|
licensePlate: store.current.licensePlate,
|
||||||
|
...datePayload
|
||||||
|
})
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepSelect = async (step: number) => {
|
||||||
|
if (!store.current) return
|
||||||
|
if (step === store.current.currentStep) return
|
||||||
|
await store[updateMethod](store.current.id, { currentStep: step })
|
||||||
|
await store[loadMethod](store.current.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const advanceStep = async () => {
|
||||||
|
if (!store.current) return
|
||||||
|
const nextStep = store.current.currentStep + 1
|
||||||
|
await store[updateMethod](store.current.id, { currentStep: nextStep })
|
||||||
|
await store[loadMethod](store.current.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepLabels,
|
||||||
|
currentStep,
|
||||||
|
entity,
|
||||||
|
init,
|
||||||
|
saveAndHold,
|
||||||
|
handleStepSelect,
|
||||||
|
advanceStep
|
||||||
|
}
|
||||||
|
}
|
||||||
25
frontend/config/reception.config.ts
Normal file
25
frontend/config/reception.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { WorkflowConfig, WorkflowEntity } from '~/types/workflow'
|
||||||
|
|
||||||
|
export const receptionConfig: WorkflowConfig = {
|
||||||
|
entityName: 'reception',
|
||||||
|
apiResource: 'receptions',
|
||||||
|
steps: [
|
||||||
|
{ label: 'Réception' },
|
||||||
|
{ label: 'Pesée à plein', weighingMode: 'gross' },
|
||||||
|
{ label: 'Sélection réception' },
|
||||||
|
{ label: 'Pesée à vide', weighingMode: 'tare', isFinal: true }
|
||||||
|
],
|
||||||
|
weighingLabels: {
|
||||||
|
gross: 'Pesée à plein',
|
||||||
|
tare: 'Pesée à vide'
|
||||||
|
},
|
||||||
|
buildReceiptFilename: (entity: WorkflowEntity) => {
|
||||||
|
const rec = entity as any
|
||||||
|
return `${rec.identificationNumber ?? rec.id}_${rec.supplier?.name ?? 'fournisseur'}_${rec.licensePlate ?? 'immat'}.pdf`
|
||||||
|
},
|
||||||
|
routePrefix: '/reception',
|
||||||
|
toastPrefix: 'reception',
|
||||||
|
dateField: 'receptionDate'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RECEPTION_STEP_LABELS = receptionConfig.steps.map(s => s.label)
|
||||||
25
frontend/config/shipment.config.ts
Normal file
25
frontend/config/shipment.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { WorkflowConfig, WorkflowEntity } from '~/types/workflow'
|
||||||
|
|
||||||
|
export const shipmentConfig: WorkflowConfig = {
|
||||||
|
entityName: 'shipment',
|
||||||
|
apiResource: 'shipments',
|
||||||
|
steps: [
|
||||||
|
{ label: 'Expédition' },
|
||||||
|
{ label: 'Pesée à vide', weighingMode: 'gross' },
|
||||||
|
{ label: 'Chargement' },
|
||||||
|
{ label: 'Pesée à plein', weighingMode: 'tare', isFinal: true }
|
||||||
|
],
|
||||||
|
weighingLabels: {
|
||||||
|
gross: 'Pesée à vide',
|
||||||
|
tare: 'Pesée à plein'
|
||||||
|
},
|
||||||
|
buildReceiptFilename: (entity: WorkflowEntity) => {
|
||||||
|
const ship = entity as any
|
||||||
|
return `${ship.identificationNumber ?? ship.id}_${ship.customer?.label ?? 'client'}_${ship.licensePlate ?? 'immat'}.pdf`
|
||||||
|
},
|
||||||
|
routePrefix: '/shipment',
|
||||||
|
toastPrefix: 'shipment',
|
||||||
|
dateField: 'shipmentDate'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SHIPMENT_STEP_LABELS = shipmentConfig.steps.map(s => s.label)
|
||||||
153
frontend/i18n/locales/fr.json
Normal file
153
frontend/i18n/locales/fr.json
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
"errors": {
|
||||||
|
"http": {
|
||||||
|
"get": "Impossible de récupérer les données.",
|
||||||
|
"post": "Impossible de créer la ressource.",
|
||||||
|
"put": "Impossible de mettre à jour la ressource.",
|
||||||
|
"patch": "Impossible de mettre à jour la ressource.",
|
||||||
|
"delete": "Impossible de supprimer la ressource."
|
||||||
|
},
|
||||||
|
"reception": {
|
||||||
|
"list": "Impossible de récupérer la liste des réceptions.",
|
||||||
|
"fetch": "Impossible de récupérer la réception.",
|
||||||
|
"create": "Impossible de créer la réception.",
|
||||||
|
"update": "Impossible de mettre à jour la réception.",
|
||||||
|
"delete": "Impossible de supprimer la réception.",
|
||||||
|
"weight": "Impossible de récupérer la pesée."
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"update": "Impossible de mettre à jour la pesée"
|
||||||
|
},
|
||||||
|
"shipment": {
|
||||||
|
"list": "Impossible de récupérer la liste des éxpeditions.",
|
||||||
|
"fetch": "Impossible de récupérer l'éxpeditions.",
|
||||||
|
"create": "Impossible de créer l'éxpeditions.",
|
||||||
|
"update": "Impossible de mettre à jour l'éxpeditions.",
|
||||||
|
"delete": "Impossible de supprimer l'expédition.",
|
||||||
|
"weigh": "Impossible de récupérer la pesée."
|
||||||
|
},
|
||||||
|
"shipmentBovine": {
|
||||||
|
"list": "Impossible de récupérer la liste des bovins de l'éxpedition.",
|
||||||
|
"create": "Impossible d'enregistrer le bovin.",
|
||||||
|
"delete": "Impossible de supprimer le bovin.",
|
||||||
|
"update": "Impossible de mettre à jour le bovin."
|
||||||
|
},
|
||||||
|
"shipmentType": {
|
||||||
|
"list": "Impossible de récupérer la liste des types d'éxpedition."
|
||||||
|
},
|
||||||
|
"receptionType": {
|
||||||
|
"list": "Impossible de récupérer la liste des types de réception."
|
||||||
|
},
|
||||||
|
"merchandiseType": {
|
||||||
|
"list": "Impossible de récupérer la liste des types de marchandises."
|
||||||
|
},
|
||||||
|
"building": {
|
||||||
|
"list": "Impossible de récupérer la liste des bâtiments."
|
||||||
|
},
|
||||||
|
"pelletType": {
|
||||||
|
"list": "Impossible de récupérer la liste des types de granulés."
|
||||||
|
},
|
||||||
|
"receptionPelletBuilding": {
|
||||||
|
"list": "Impossible de récupérer la liste des dépôts de granulés.",
|
||||||
|
"create": "Impossible d'enregistrer le dépôt de granulés.",
|
||||||
|
"delete": "Impossible de supprimer le dépôt de granulés."
|
||||||
|
},
|
||||||
|
"receptionBovine": {
|
||||||
|
"list": "Impossible de récupérer la liste des bovins de la réception.",
|
||||||
|
"create": "Impossible d'enregistrer le bovin.",
|
||||||
|
"delete": "Impossible de supprimer le bovin."
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"list": "Impossible de récupérer la liste des fournisseurs.",
|
||||||
|
"fetch": "Impossible de récupérer le fournisseur.",
|
||||||
|
"create": "Impossible de créer le fournisseur.",
|
||||||
|
"update": "Impossible de mettre à jour le fournisseur.",
|
||||||
|
"nameRequired": "Le nom du fournisseur est obligatoire."
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"fetch": "Impossible de récupérer l'adresse.",
|
||||||
|
"create": "Impossible de créer l'adresse.",
|
||||||
|
"update": "Impossible de mettre à jour l'adresse.",
|
||||||
|
"entityNotFound": "Entité introuvable.",
|
||||||
|
"streetRequired": "La rue est obligatoire.",
|
||||||
|
"postalCodeRequired": "Le code postal est obligatoire.",
|
||||||
|
"cityRequired": "La ville est obligatoire.",
|
||||||
|
"countryCodeInvalid": "Le pays doit être un code ISO2 (2 lettres)."
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"list": "Impossible de récupérer la liste des clients.",
|
||||||
|
"fetch": "Impossible de récupérer le client.",
|
||||||
|
"create": "Impossible de créer le client.",
|
||||||
|
"update": "Impossible de mettre à jour le client."
|
||||||
|
},
|
||||||
|
"truck": {
|
||||||
|
"list": "Impossible de récupérer la liste des camions."
|
||||||
|
},
|
||||||
|
"bovin": {
|
||||||
|
"list": "Impossible de récupérer la liste des races de bovins.",
|
||||||
|
"fetch": "Impossible de récupérer le type bovin.",
|
||||||
|
"create": "Impossible de créer le type bovin.",
|
||||||
|
"update": "Impossible de mettre à jour le type bovin."
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"list": "Impossible de récupérer la liste des transporteurs.",
|
||||||
|
"fetch": "Impossible de récupérer les données du transporteur",
|
||||||
|
"update": "Impossible de mettre à jour le transporteur",
|
||||||
|
"create": "Impossible de créer le transporteur"
|
||||||
|
},
|
||||||
|
"driver": {
|
||||||
|
"list": "Impossible de récupérer la liste des chauffeurs."
|
||||||
|
},
|
||||||
|
"vehicle": {
|
||||||
|
"list": "Impossible de récupérer la liste des immatriculations."
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": "Identifiants invalides.",
|
||||||
|
"users": "Impossible de récupérer les utilisateurs.",
|
||||||
|
"logout": "Impossible de se déconnecter.",
|
||||||
|
"update": "Impossible de mettre à jour l'utilisateur.",
|
||||||
|
"create": "Impossible de créer l'utilisateur."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"reception": {
|
||||||
|
"create": "Réception créée avec succès",
|
||||||
|
"update": "Réception mise à jour avec succès.",
|
||||||
|
"delete": "Réception supprimée avec succès."
|
||||||
|
},
|
||||||
|
"shipment": {
|
||||||
|
"create": "Éxpedition créée avec succès",
|
||||||
|
"update": "Éxpedition mise à jour avec succès.",
|
||||||
|
"delete": "Expédition supprimée avec succès."
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"create": "Fournisseur créé avec succès.",
|
||||||
|
"update": "Fournisseur mis à jour avec succès."
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"create": "Client créé avec succès.",
|
||||||
|
"update": "Client mis à jour avec succès."
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"create": "Adresse créée avec succès.",
|
||||||
|
"update": "Adresse mise à jour avec succès."
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"update": "Utilisateur mis à jour avec succès.",
|
||||||
|
"create": "Utilisateur créé avec succès.",
|
||||||
|
"login": "Connexion réussie.",
|
||||||
|
"logout": "Déconnexion réussie."
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"update": "Transporteur mis à jour",
|
||||||
|
"create": "Transporteur créé"
|
||||||
|
},
|
||||||
|
"bovin": {
|
||||||
|
"update": "Type bovin mis à jour avec succès.",
|
||||||
|
"create": "Type bovin créé avec succès."
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"update": "Pesée mis à jour"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/layouts/auth.vue
Normal file
7
frontend/layouts/auth.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-primary-500 from-primary-50 via-white to-neutral-100 text-neutral-900">
|
||||||
|
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
270
frontend/layouts/default.vue
Normal file
270
frontend/layouts/default.vue
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen text-neutral-900 flex flex-col">
|
||||||
|
<!-- HEADER -->
|
||||||
|
<header class="w-full bg-primary-500 py-5 px-6">
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<!-- Burger (mobile) -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center text-3xl text-white md:hidden"
|
||||||
|
aria-label="Ouvrir le menu"
|
||||||
|
@click="toggleMenu"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" class="flex items-center">
|
||||||
|
<Icon name="mdi:menu" size="44"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Logo -->
|
||||||
|
<NuxtLink to="/" class="shrink-0">
|
||||||
|
<span class="flex items-center justify-center bg-white text-xl font-bold uppercase px-6 py-4">
|
||||||
|
LOGO
|
||||||
|
</span>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- NAV centré (desktop) -->
|
||||||
|
<nav
|
||||||
|
class="hidden md:flex flex-1 items-center justify-center gap-8 text-xl font-bold uppercase text-white"
|
||||||
|
>
|
||||||
|
<NuxtLink to="/" custom v-slot="{ href, navigate }">
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path === '/'
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Accueil
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/user/list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/user')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Utilisateurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/supplier/supplier-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/supplier')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Fournisseurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/carrier/carrier-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/carrier')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Transporteurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/customer/customer-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/customer')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Clients
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/bovin/bovin-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/bovin')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Bovins
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Spacer mobile (pour centrer visuellement le header si besoin) -->
|
||||||
|
<div class="w-[44px] md:hidden"></div>
|
||||||
|
|
||||||
|
<!-- User dropdown à droite (desktop) -->
|
||||||
|
<div v-if="auth.isAuthenticated" class="ml-auto relative hidden md:flex items-center text-white group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center py-2 -my-2 text-xl leading-none transition hover:opacity-80"
|
||||||
|
aria-haspopup="true"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:account-circle-outline" class="self-center" size="36"/>
|
||||||
|
<span class="capitalize font-bold ml-4">{{ userDisplayName }}</span>
|
||||||
|
<span
|
||||||
|
class="ml-[6px] inline-flex items-center font-bold transition-transform group-hover:rotate-180 group-focus-within:rotate-180">
|
||||||
|
<Icon name="mdi:chevron-down" size="20"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute right-0 top-full z-10 w-56 rounded-md bg-primary-500 py-2 border-neutral-300 border shadow-lg
|
||||||
|
opacity-0 invisible pointer-events-none transition
|
||||||
|
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto
|
||||||
|
group-focus-within:opacity-100 group-focus-within:visible group-focus-within:pointer-events-auto"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-4 py-2 text-left text-sm font-semibold text-white opacity-85 hover:opacity-100 transition"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay (mobile) -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0"
|
||||||
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="transition duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isMenuOpen"
|
||||||
|
class="fixed inset-0 z-40 bg-black/40 md:hidden"
|
||||||
|
@click="closeMenu"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Drawer (mobile) -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition duration-200 ease-out"
|
||||||
|
enter-from-class="-translate-x-full"
|
||||||
|
enter-to-class="translate-x-0"
|
||||||
|
leave-active-class="transition duration-150 ease-in"
|
||||||
|
leave-from-class="translate-x-0"
|
||||||
|
leave-to-class="-translate-x-full"
|
||||||
|
>
|
||||||
|
<aside
|
||||||
|
v-if="isMenuOpen"
|
||||||
|
class="fixed left-0 top-0 z-50 h-full w-full bg-primary-500 px-6 pb-8 pt-6 text-white shadow-xl md:hidden"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-2xl font-bold uppercase">Menu</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-2xl"
|
||||||
|
aria-label="Fermer le menu"
|
||||||
|
@click="closeMenu"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:close" size="44"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase">
|
||||||
|
<NuxtLink to="/admin/dashboard" @click="closeMenu">Accueil</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/supplier/supplier-list" @click="closeMenu">
|
||||||
|
Fournisseurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/carrier/carrier-list" @click="closeMenu">
|
||||||
|
Transporteurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/user/list" @click="closeMenu">
|
||||||
|
Utilisateurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/customer/customer-list" @click="closeMenu">
|
||||||
|
Clients
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/bovin/bovin-list" @click="closeMenu">
|
||||||
|
Bovins
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="auth.isAuthenticated"
|
||||||
|
type="button"
|
||||||
|
class="mt-6 text-xl font-bold uppercase"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
</transition>
|
||||||
|
</header>
|
||||||
|
<main class="mx-auto w-full max-w-[1280px] mt-16">
|
||||||
|
<slot/>
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto bg-primary-500 px-6 py-3">
|
||||||
|
<p class="font-bold text-white text-right">v{{ version }}</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const {version} = useAppVersion()
|
||||||
|
|
||||||
|
const isMenuOpen = ref(false)
|
||||||
|
|
||||||
|
const userDisplayName = computed(() => auth.user?.username ?? 'Utilisateur')
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
isMenuOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
isMenuOpen.value = !isMenuOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await auth.logout()
|
||||||
|
} finally {
|
||||||
|
closeMenu()
|
||||||
|
await navigateTo('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
17
frontend/middleware/auth.global.ts
Normal file
17
frontend/middleware/auth.global.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
if (to.path === '/login') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated) {
|
||||||
|
await auth.ensureSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated) {
|
||||||
|
return navigateTo('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -2,7 +2,38 @@ export default defineNuxtConfig({
|
|||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
ssr: false,
|
ssr: false,
|
||||||
modules: ['@nuxtjs/tailwindcss'],
|
app: {
|
||||||
|
baseURL: process.env.NUXT_PUBLIC_APP_BASE || '/'
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'nuxt-toast',
|
||||||
|
'@nuxtjs/i18n',
|
||||||
|
'@nuxt/icon'
|
||||||
|
],
|
||||||
|
css: ['~/assets/css/main.css', '~/assets/css/toast.css'],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBase: process.env.NUXT_PUBLIC_API_BASE,
|
||||||
|
geoApiBase: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toast: {
|
||||||
|
settings: {
|
||||||
|
timeout: 10000,
|
||||||
|
closeOnClick: true,
|
||||||
|
progressBar: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
strategy: 'no_prefix',
|
||||||
|
defaultLocale: 'fr',
|
||||||
|
langDir: 'locales',
|
||||||
|
locales: [
|
||||||
|
{ code: 'fr', file: 'fr.json', name: 'Français' }
|
||||||
|
]
|
||||||
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
strict: true
|
strict: true
|
||||||
}
|
}
|
||||||
|
|||||||
2564
frontend/package-lock.json
generated
2564
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,17 @@
|
|||||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nuxt/icon": "^2.2.1",
|
||||||
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
"izitoast": "^1.4.0",
|
||||||
|
"maska": "^3.2.0",
|
||||||
"nuxt": "^4.2.2",
|
"nuxt": "^4.2.2",
|
||||||
|
"nuxt-toast": "^1.4.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4",
|
||||||
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0"
|
"@nuxtjs/tailwindcss": "^6.14.0"
|
||||||
|
|||||||
112
frontend/pages/admin/bovin/[[id]].vue
Normal file
112
frontend/pages/admin/bovin/[[id]].vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
<div class="flex items-center justify-between relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
|
<Icon
|
||||||
|
@click="router.push('/admin/bovin/bovin-list')"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="40"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ route.params.id ? 'Modifications du type bovin' : 'Ajout d\'un type bovin' }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]">
|
||||||
|
<UiTextInput label="Nom du bovin" id="bovin-label" v-model="form.label" required />
|
||||||
|
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || isHydrating"
|
||||||
|
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {createBovin, getBovin, updateBovin} from "~/services/bovine-type";
|
||||||
|
import type {BovineTypeData, BovinFormData} from "~/services/dto/bovine-type-data";
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
const idBovin = computed(() => resolveId(route.params.id))
|
||||||
|
const isEdit = computed(() => idBovin.value !== null)
|
||||||
|
|
||||||
|
function resolveId(param: unknown) {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<BovinFormData>({
|
||||||
|
label: '',
|
||||||
|
code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const hydrateFromBovin = (bovin: BovineTypeData | null) => {
|
||||||
|
if (!bovin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.label = bovin.label ?? ''
|
||||||
|
form.code = bovin.code ?? ''
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => idBovin.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const bovin = await getBovin(id)
|
||||||
|
hydrateFromBovin(bovin)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value || isHydrating.value) return
|
||||||
|
|
||||||
|
const normalizedBovinCode = form.code.trim()
|
||||||
|
const normalizedBovinLabel = form.label.trim()
|
||||||
|
|
||||||
|
|
||||||
|
const basePayload = {
|
||||||
|
label: normalizedBovinLabel,
|
||||||
|
code: normalizedBovinCode
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
if (isEdit.value && idBovin.value !== null) {
|
||||||
|
await updateBovin(idBovin.value, basePayload)
|
||||||
|
} else {
|
||||||
|
await createBovin(basePayload)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigate(){
|
||||||
|
return router.push("/admin/bovin/list")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
67
frontend/pages/admin/bovin/bovin-list.vue
Normal file
67
frontend/pages/admin/bovin/bovin-list.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between ">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des types bovins</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mt-7 border border-slate-200 mb-11 ">
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-primary-700 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Code</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!auth.isAdmin" class="px-4 py-6 text-slate-400">
|
||||||
|
Accès réservé aux administrateurs.
|
||||||
|
</div>
|
||||||
|
<div v-else-if="bovinList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun type de bovin.
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="bovin in bovinList"
|
||||||
|
:key="bovin.id"
|
||||||
|
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToBovin(bovin.id)"
|
||||||
|
@keydown.enter="goToBovin(bovin.id)"
|
||||||
|
>
|
||||||
|
<div>{{ bovin.label }}</div>
|
||||||
|
<div>{{ bovin.code }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/bovin"
|
||||||
|
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getBovineTypeList } from "~/services/bovine-type"
|
||||||
|
import type { BovineTypeData } from "~/services/dto/bovine-type-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const bovinList = ref<BovineTypeData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToBovin = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/bovin/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
bovinList.value = await getBovineTypeList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
117
frontend/pages/admin/carrier/[[id]].vue
Normal file
117
frontend/pages/admin/carrier/[[id]].vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
<div class="flex items-center justify-between relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
|
<Icon
|
||||||
|
@click="router.push('/admin/carrier/carrier-list')"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="40"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ route.params.id ? 'Modification du transporteur' : 'Ajout d\'un transporteur' }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]">
|
||||||
|
<UiTextInput
|
||||||
|
label="Nom du transporteur"
|
||||||
|
id="carrier-name"
|
||||||
|
v-model="form.name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiTextInput
|
||||||
|
label="Code transporteur"
|
||||||
|
id="code-id"
|
||||||
|
v-model="form.code"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier";
|
||||||
|
import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data";
|
||||||
|
import {computed} from "vue";
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const idCarrier = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<CarrierFormData>({
|
||||||
|
code:'',
|
||||||
|
name:''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hydrateFromUser = (carrier: CarrierData | null) => {
|
||||||
|
if (!carrier) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.name = carrier.name ?? ''
|
||||||
|
form.code = carrier.code ?? ''
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => idCarrier.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const user = await getCarrier(id)
|
||||||
|
hydrateFromUser(user)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
async function validate() {
|
||||||
|
|
||||||
|
const normalizedCarrierCode = form.code.trim()
|
||||||
|
const normalizedCarrierName = form.name.trim()
|
||||||
|
|
||||||
|
const basePayload = {
|
||||||
|
name: normalizedCarrierName,
|
||||||
|
code: normalizedCarrierCode
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idCarrier.value){
|
||||||
|
await updateCarrier(idCarrier.value, basePayload)
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
await createCarrier(basePayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(){
|
||||||
|
router.push("/admin/carrier/carrier-list")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
51
frontend/pages/admin/carrier/carrier-list.vue
Normal file
51
frontend/pages/admin/carrier/carrier-list.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between ">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">listes des transporteurs</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-7 border border-slate-200 mb-11 ">
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-primary-700 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Label</div>
|
||||||
|
<div>Code</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="carrier in carrierList"
|
||||||
|
:key="carrier.id"
|
||||||
|
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToCarrier(carrier.id)"
|
||||||
|
@keydown.enter="goToCarrier(carrier.id)"
|
||||||
|
>
|
||||||
|
<div>{{ carrier.name}}</div>
|
||||||
|
<div>{{ carrier.code }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/carrier"
|
||||||
|
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {CarrierData} from "~/services/dto/carrier-data";
|
||||||
|
import {getCarrierList} from "~/services/carrier";
|
||||||
|
|
||||||
|
const carrierList = ref<CarrierData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goToCarrier = (id: number) => {
|
||||||
|
router.push(`/admin/carrier/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
carrierList.value = await getCarrierList(false)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
205
frontend/pages/admin/customer/[[id]].vue
Normal file
205
frontend/pages/admin/customer/[[id]].vue
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
|
<Icon @click="router.push('/admin/customer/customer-list')" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ customerId ? "Modification du client" : "Ajout d'un client" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
|
<UiTextInput id="customer-name" v-model="form.name" label="Nom du client" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
|
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
|
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
<Icon :name="customerId ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ customerId ? "Valider" : "Ajouter" }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-7">
|
||||||
|
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du client</h2>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto mb-11 text-primary-700">
|
||||||
|
<table class="w-full border-collapse text-primary-700">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border bg-slate-100 border-gray-200">
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Rue</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Complément</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Code postal</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Ville</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Pays</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-if="form.addresses.length === 0">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="py-4 text-slate-400">
|
||||||
|
Aucune adresse.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr
|
||||||
|
v-for="(address, index) in form.addresses"
|
||||||
|
:key="address.id ?? index"
|
||||||
|
class="border border-gray-100 hover:bg-slate-50"
|
||||||
|
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="goToEditAddress(address.id ?? null)"
|
||||||
|
>
|
||||||
|
<td class="py-3 px-4">{{ address.street || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.street2 || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.postalCode || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.city || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.countryCode || "—" }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center text-xl gap-2 text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
:disabled="customerId === null || !auth.isAdmin"
|
||||||
|
@click="goToAddAddress"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
|
import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
|
||||||
|
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
|
||||||
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
const customerId = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
const form = reactive<CustomerFormData>({
|
||||||
|
name: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
addresses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToAddAddress = () => {
|
||||||
|
if (customerId.value === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToEditAddress = (addressId: number | null) => {
|
||||||
|
if (customerId.value === null || addressId === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
addressId: String(addressId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFromCustomer = (customer: CustomerData | null) => {
|
||||||
|
if (!customer) return
|
||||||
|
form.name = customer.name ?? ""
|
||||||
|
form.phone = customer.phone ?? ""
|
||||||
|
form.email = customer.email ?? ""
|
||||||
|
if (!Array.isArray(customer.addresses) || customer.addresses.length === 0) {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof customer.addresses[0] === "string") {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addresses = customer.addresses.map((address) => ({
|
||||||
|
id: address.id ?? null,
|
||||||
|
street: address.street ?? "",
|
||||||
|
street2: address.street2 ?? null,
|
||||||
|
postalCode: address.postalCode ?? "",
|
||||||
|
city: address.city ?? "",
|
||||||
|
countryCode: address.countryCode ?? "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => customerId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const customer = await getCustomer(id)
|
||||||
|
hydrateFromCustomer(customer)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = form.name.trim()
|
||||||
|
const phone = form.phone?.trim() || null
|
||||||
|
const email = form.email?.trim() || null
|
||||||
|
|
||||||
|
const customerPayload: CustomerPayload = {
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
|
if (customerId.value !== null) {
|
||||||
|
await updateCustomer(customerId.value, customerPayload)
|
||||||
|
targetId = customerId.value
|
||||||
|
} else {
|
||||||
|
const creationPayload = {
|
||||||
|
...customerPayload,
|
||||||
|
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
||||||
|
}
|
||||||
|
const created = await createCustomer(creationPayload)
|
||||||
|
targetId = created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId !== null) {
|
||||||
|
await router.push(`/admin/customer/${targetId}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
44
frontend/pages/admin/customer/address.vue
Normal file
44
frontend/pages/admin/customer/address.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<Address type="customer" :address="address" @validate="validate"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AddressData, AddressPayload } from "~/services/address"
|
||||||
|
import { createAddress, getAddress, updateAddress } from "~/services/address"
|
||||||
|
import { getCustomer, updateCustomer } from "~/services/customer"
|
||||||
|
import type { CustomerData } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const customerId = computed(() => Number(route.query.customerId))
|
||||||
|
const customer = ref<CustomerData | null>(null)
|
||||||
|
const addressId = computed(() => (route.query.addressId !== undefined ? Number(route.query.addressId) : null))
|
||||||
|
const address = ref<AddressData | null>(null)
|
||||||
|
|
||||||
|
const validate = async (payload: AddressPayload) => {
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
await updateAddress(addressId.value, payload)
|
||||||
|
} else {
|
||||||
|
await addAddress(payload)
|
||||||
|
await router.push("/admin/customer/" + customerId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = async (payload: AddressPayload) => {
|
||||||
|
const response: AddressData = await createAddress(payload)
|
||||||
|
const addressIRI = `/api/addresses/${response.id}`
|
||||||
|
const existingIris = (customer.value?.addresses ?? [])
|
||||||
|
.map((item: any) => (typeof item === "string" ? item : `/api/addresses/${item.id}`))
|
||||||
|
.filter((iri: string | null) => Boolean(iri)) as string[]
|
||||||
|
const next = [...new Set([...existingIris, addressIRI])]
|
||||||
|
|
||||||
|
return await updateCustomer(customerId.value, { addresses: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
customer.value = await getCustomer(customerId.value)
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
address.value = await getAddress(addressId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
73
frontend/pages/admin/customer/customer-list.vue
Normal file
73
frontend/pages/admin/customer/customer-list.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des clients</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
|
<div class="max-h-96 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="sticky text-primary-700 top-0 z-10 grid grid-cols-4 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Téléphone</div>
|
||||||
|
<div>Mail</div>
|
||||||
|
<div>Créé par</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun client.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="customer in customerList"
|
||||||
|
:key="customer.id"
|
||||||
|
class="grid grid-cols-4 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
@click="goToCustomer(customer.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ customer.name || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.phone || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.email || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.createdBy?.username || "—" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
|
||||||
|
Accès réservé aux administrateurs.
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/customer"
|
||||||
|
class="inline-flex items-center mb-16 justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getCustomerList } from "~/services/customer"
|
||||||
|
import type { CustomerData } from "~/services/dto/customer-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const customerList = ref<CustomerData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToCustomer = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/customer/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
customerList.value = await getCustomerList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
206
frontend/pages/admin/supplier/[[id]].vue
Normal file
206
frontend/pages/admin/supplier/[[id]].vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
|
<Icon @click="router.push('/admin/supplier/supplier-list')" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ supplierId ? "Modification du fournisseur" : "Ajout d'un fournisseur" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
|
<UiTextInput id="supplier-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
|
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
|
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
<Icon :name="supplierId ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ supplierId ? "Valider" : "Ajouter" }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-7">
|
||||||
|
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du fournisseur</h2>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto mb-11 text-primary-700">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border bg-slate-100 border-gray-200">
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Rue</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Complément</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Code postal</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Ville</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Pays</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-if="form.addresses.length === 0">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="py-4 text-slate-400">
|
||||||
|
Aucune adresse.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr
|
||||||
|
v-for="(address, index) in form.addresses"
|
||||||
|
:key="address.id ?? index"
|
||||||
|
class="border border-gray-100 hover:bg-slate-50"
|
||||||
|
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="goToEditAddress(address.id ?? null)"
|
||||||
|
>
|
||||||
|
<td class="py-3 px-4">{{ address.street || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.street2 || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.postalCode || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.city || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.countryCode || "—" }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center text-xl gap-2 text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
:disabled="supplierId === null || !auth.isAdmin"
|
||||||
|
@click="goToAddAddress"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
|
import {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
|
||||||
|
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
|
||||||
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
const supplierId = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
const form = reactive<SupplierFormData>({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
addresses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToAddAddress = () => {
|
||||||
|
if (supplierId.value === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/supplier/address",
|
||||||
|
query: {
|
||||||
|
supplierId: String(supplierId.value),
|
||||||
|
fromSupplier: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToEditAddress = (addressId: number | null) => {
|
||||||
|
if (supplierId.value === null || addressId === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/supplier/address",
|
||||||
|
query: {
|
||||||
|
supplierId: String(supplierId.value),
|
||||||
|
addressId: String(addressId),
|
||||||
|
fromSupplier: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFromSupplier = (supplier: SupplierData | null) => {
|
||||||
|
if (!supplier) return
|
||||||
|
form.name = supplier.name ?? ""
|
||||||
|
form.email = supplier.email ?? ""
|
||||||
|
form.phone = supplier.phone ?? ""
|
||||||
|
if (!Array.isArray(supplier.addresses) || supplier.addresses.length === 0) {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof supplier.addresses[0] === "string") {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addresses = supplier.addresses.map((address) => ({
|
||||||
|
id: address.id ?? null,
|
||||||
|
street: address.street ?? "",
|
||||||
|
street2: address.street2 ?? null,
|
||||||
|
postalCode: address.postalCode ?? "",
|
||||||
|
city: address.city ?? "",
|
||||||
|
countryCode: address.countryCode ?? "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => supplierId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const supplier = await getSupplier(id)
|
||||||
|
hydrateFromSupplier(supplier)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = form.name.trim()
|
||||||
|
const email = (form.email ?? "").trim() || null
|
||||||
|
const phone = (form.phone ?? "").trim() || null
|
||||||
|
|
||||||
|
const supplierPayload: SupplierPayload = {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
|
if (supplierId.value !== null) {
|
||||||
|
await updateSupplier(supplierId.value, supplierPayload)
|
||||||
|
targetId = supplierId.value
|
||||||
|
} else {
|
||||||
|
const creationPayload = {
|
||||||
|
...supplierPayload,
|
||||||
|
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
||||||
|
}
|
||||||
|
const created = await createSupplier(creationPayload)
|
||||||
|
targetId = created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.push(`/admin/supplier/${targetId}`)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
42
frontend/pages/admin/supplier/address.vue
Normal file
42
frontend/pages/admin/supplier/address.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<Address type="supplier" :address="address" @validate="validate"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {AddressData, AddressPayload} from "~/services/address";
|
||||||
|
import {createAddress, getAddress, updateAddress} from "~/services/address";
|
||||||
|
import {getSupplier, updateSupplier} from "~/services/supplier";
|
||||||
|
import type {SupplierData} from "~/services/dto/supplier-data";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const supplierId = computed(() => { return Number(route.query.supplierId) })
|
||||||
|
const supplier = ref<SupplierData|null>(null);
|
||||||
|
const addressId = computed(() => { return route.query.addressId !== undefined ? Number(route.query.addressId) : null })
|
||||||
|
const address = ref<AddressData|null>(null)
|
||||||
|
|
||||||
|
const validate = async (address: AddressPayload) => {
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
await updateAddress(addressId.value, address)
|
||||||
|
} else {
|
||||||
|
await addAddress(address)
|
||||||
|
await router.push('/admin/supplier/' + supplierId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = async (address: AddressPayload) => {
|
||||||
|
const response: AddressData = await createAddress(address)
|
||||||
|
const addressIRI = `/api/addresses/${response.id}`
|
||||||
|
const existingIris = (supplier.value.addresses ?? []).map((item: any) => `/api/addresses/${item.id}`)
|
||||||
|
const next = [...new Set([...existingIris, addressIRI])]
|
||||||
|
|
||||||
|
return await updateSupplier(supplierId.value, { addresses: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
supplier.value = await getSupplier(supplierId.value)
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
address.value = await getAddress(addressId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
73
frontend/pages/admin/supplier/supplier-list.vue
Normal file
73
frontend/pages/admin/supplier/supplier-list.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des fournisseurs</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
|
<div class="max-h-96 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="sticky text-primary-700 top-0 z-10 grid grid-cols-4 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Téléphone</div>
|
||||||
|
<div>Mail</div>
|
||||||
|
<div>Créé par</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="supplierList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun fournisseur.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="supplier in supplierList"
|
||||||
|
:key="supplier.id"
|
||||||
|
class="grid grid-cols-4 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ supplier.name || "—" }}</div>
|
||||||
|
<div class="truncate">{{ supplier.phone || "—" }}</div>
|
||||||
|
<div class="truncate">{{ supplier.email || "—" }}</div>
|
||||||
|
<div class="truncate">{{ supplier.createdBy?.username || "—" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
|
||||||
|
Accès réservé aux administrateurs.
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/supplier"
|
||||||
|
class="inline-flex items-center mb-16 justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getSupplierList } from "~/services/supplier"
|
||||||
|
import type { SupplierData } from "~/services/dto/supplier-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const supplierList = ref<SupplierData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToSupplier = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/supplier/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
supplierList.value = await getSupplierList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
147
frontend/pages/admin/user/[[id]].vue
Normal file
147
frontend/pages/admin/user/[[id]].vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
|
<Icon
|
||||||
|
@click="router.push('/admin/user/list')"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="40"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ userId ? "Modification de l'utilisateur" : "Ajout d'un utilisateur" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
|
<UiTextInput
|
||||||
|
id="user-name"
|
||||||
|
v-model="form.username"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="user-role"
|
||||||
|
v-model="form.role"
|
||||||
|
label="Role de l'utilisateur"
|
||||||
|
:options="ROLE"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiTextInput
|
||||||
|
id="user-password"
|
||||||
|
v-model="form.password"
|
||||||
|
label="Mot de passe"
|
||||||
|
type="password"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || isHydrating || !auth.isAdmin"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
<Icon :name="userId ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ userId ? 'Valider' : 'Ajouter' }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import { ROLE } from '~/utils/constants'
|
||||||
|
import { createUser, updateUser, getUser } from '~/services/auth'
|
||||||
|
import type { UserData, UserFormData, UserPayload } from '~/services/dto/user-data'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const userId = computed(() => resolveUserId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
|
const resolveUserId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<UserFormData>({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
role: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hydrateFromUser = (user: UserData | null) => {
|
||||||
|
if (!user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.username = user.username ?? ''
|
||||||
|
const roles = user.roles ?? []
|
||||||
|
const hasAdmin = roles.includes('ROLE_ADMIN')
|
||||||
|
form.role = hasAdmin ? 'ROLE_ADMIN' : 'ROLE_USER'
|
||||||
|
form.password = ''
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => userId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const user = await getUser(id)
|
||||||
|
hydrateFromUser(user)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
|
||||||
|
const normalizedUsername = form.username.trim()
|
||||||
|
const normalizedRole = form.role.trim()
|
||||||
|
const normalizedPassword = form.password.trim()
|
||||||
|
|
||||||
|
const basePayload: UserPayload = {
|
||||||
|
username: normalizedUsername,
|
||||||
|
roles: normalizedRole ? [normalizedRole] : undefined,
|
||||||
|
}
|
||||||
|
if (normalizedPassword) {
|
||||||
|
basePayload.password = normalizedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId.value) {
|
||||||
|
await updateUser(userId.value, basePayload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await createUser(basePayload)
|
||||||
|
if (created) {
|
||||||
|
await router.push('/admin/user/list')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
81
frontend/pages/admin/user/list.vue
Normal file
81
frontend/pages/admin/user/list.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des utilisateurs</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
|
<div class="grid grid-cols-2 text-primary-700 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Utilisateur</div>
|
||||||
|
<div>Role</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="userList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun utilisateur.
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="user in userList"
|
||||||
|
:key="user.id"
|
||||||
|
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200 items-center"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToUser(user.id)"
|
||||||
|
@keydown.enter="goToUser(user.id)"
|
||||||
|
>
|
||||||
|
<div>{{ user.username }}</div>
|
||||||
|
<div>{{ getRoleLabels(user.roles) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
|
||||||
|
Acces reserve aux administrateurs.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/user"
|
||||||
|
class="inline-flex items-center mb-16 justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { UserData } from "~/services/dto/user-data"
|
||||||
|
import { getAdminUsers } from "~/services/auth"
|
||||||
|
import { ROLE } from "~/utils/constants"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const userList = ref<UserData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const roleLabelByValue = new Map(ROLE.map((role) => [role.value, role.label]))
|
||||||
|
|
||||||
|
const goToUser = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/user/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRoleLabels = (roles?: string[]) => {
|
||||||
|
if (!roles || roles.length === 0) {
|
||||||
|
return '---'
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles
|
||||||
|
.map((role) => roleLabelByValue.get(role) ?? role)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
userList.value = await getAdminUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex items-center justify-center">
|
<div class="flex flex-wrap justify-center pb-16 gap-12">
|
||||||
<h1 class="text-3xl font-bold">Nuxt OK ✅</h1>
|
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
||||||
|
<card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" />
|
||||||
|
<card-link label="PLAN DE SITE" link="/infrastructure/building" iconName="material-symbols:warehouse-outline-rounded" />
|
||||||
|
<card-link link="/reception/waiting-reception" iconName="mdi:truck-remove-outline">
|
||||||
|
<template #label>
|
||||||
|
Réceptions<br>EN ATTENTE
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
|
<card-link link="/shipment/waiting-shipment" iconName="mdi:truck-cargo-container">
|
||||||
|
<template #label>
|
||||||
|
EXPÉDITIONS<br>EN ATTENTE
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
|
<card-link label="CASES" link="/infrastructure/case" iconName="material-symbols:bottom-sheets-outline" />
|
||||||
|
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
|
||||||
|
<card-link label="EXPÉDITIONS FINIES" link="/shipment/finish-shipment" iconName="mdi:truck-delivery-outline" />
|
||||||
|
<card-link link="/" iconName="mdi:cow">
|
||||||
|
<template #label>
|
||||||
|
PASSEPORT<br>DU BOVIN
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user