Compare commits
123 Commits
feat/202-c
...
v0.0.70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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=
|
||||||
|
|||||||
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=/ 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>
|
||||||
11
.idea/ferme.iml
generated
11
.idea/ferme.iml
generated
@@ -145,6 +145,17 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sabberworm/php-css-parser" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/sabberworm/php-css-parser" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/thecodingmachine/safe" />
|
<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" />
|
||||||
|
|||||||
289
.idea/php.xml
generated
289
.idea/php.xml
generated
@@ -4,156 +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/http-client-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
||||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
<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/sabberworm/php-css-parser" />
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
<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>
|
||||||
@@ -162,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>
|
||||||
|
|||||||
839
.idea/workspace.xml
generated
839
.idea/workspace.xml
generated
@@ -4,13 +4,17 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package-lock.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/components/shipment/shipment-loading.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/shipment/shipment-loading.vue" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/components/shipment/shipment-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/shipment/shipment-weight.vue" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/composables/useWeighing.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/useWeighing.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/layouts/default.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/layouts/default.vue" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/services/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/services/shipment.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/shipment.ts" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -21,6 +25,11 @@
|
|||||||
<pharConfigPath>$PROJECT_DIR$/composer.json</pharConfigPath>
|
<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">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="151" />
|
<option name="cachedIndexableFilesCount" value="151" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
@@ -29,6 +38,7 @@
|
|||||||
<option name="RECENT_TEMPLATES">
|
<option name="RECENT_TEMPLATES">
|
||||||
<list>
|
<list>
|
||||||
<option value="TypeScript File" />
|
<option value="TypeScript File" />
|
||||||
|
<option value="PHP File" />
|
||||||
<option value="Vue Composition API Component" />
|
<option value="Vue Composition API Component" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
@@ -36,11 +46,14 @@
|
|||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="develop" />
|
<entry key="$PROJECT_DIR$" value="feat/327-voir-modifier-une-expedition-terminee" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
</component>
|
||||||
<component name="McpProjectServerCommands">
|
<component name="McpProjectServerCommands">
|
||||||
<commands />
|
<commands />
|
||||||
<urls />
|
<urls />
|
||||||
@@ -54,147 +67,157 @@
|
|||||||
</server>
|
</server>
|
||||||
</servers>
|
</servers>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpWorkspaceProjectConfiguration">
|
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="C:/php-8.4.3/php.exe">
|
||||||
<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/http-client-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
||||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
<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/sabberworm/php-css-parser" />
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
<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="ProjectColorInfo">{
|
<component name="ProjectColorInfo">{
|
||||||
@@ -214,37 +237,53 @@
|
|||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "feat/reception-generation-bon",
|
"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.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "reference.webide.settings.project.settings.php.debug",
|
"settings.editor.selected.configurable": "advanced.settings",
|
||||||
|
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
},
|
},
|
||||||
"keyToStringList": {
|
"keyToStringList": {
|
||||||
|
"DatabaseDriversLRU": [
|
||||||
|
"postgresql"
|
||||||
|
],
|
||||||
|
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||||
|
"TEXT"
|
||||||
|
],
|
||||||
"vue.recent.templates": [
|
"vue.recent.templates": [
|
||||||
"Vue Composition API Component"
|
"Vue Composition API Component"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}</component>
|
}</component>
|
||||||
<component name="RecentsManager">
|
<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">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<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="\\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" />
|
||||||
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages" />
|
|
||||||
</key>
|
</key>
|
||||||
</component>
|
</component>
|
||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
<set>
|
<set>
|
||||||
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.29346.257" />
|
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.31033.138" />
|
||||||
</set>
|
</set>
|
||||||
</attachedChunks>
|
</attachedChunks>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
<component name="TaskManager">
|
<component name="TaskManager">
|
||||||
<task active="true" id="Default" summary="Default task">
|
<task active="true" id="Default" summary="Default task">
|
||||||
<changelist id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="" />
|
<changelist id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="" />
|
||||||
@@ -258,80 +297,433 @@
|
|||||||
<workItem from="1768374298711" duration="12403000" />
|
<workItem from="1768374298711" duration="12403000" />
|
||||||
<workItem from="1768460547451" duration="26946000" />
|
<workItem from="1768460547451" duration="26946000" />
|
||||||
<workItem from="1768547023783" duration="11371000" />
|
<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="1794000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
|
<task id="LOCAL-00023" summary="feat : Ajout du bundle Monolog pour la gestion des logs">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768237763998</created>
|
<created>1769075990984</created>
|
||||||
<option name="number" value="00001" />
|
<option name="number" value="00023" />
|
||||||
<option name="presentableId" value="LOCAL-00001" />
|
<option name="presentableId" value="LOCAL-00023" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768237763998</updated>
|
<updated>1769075990984</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00002" summary="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception">
|
<task id="LOCAL-00024" summary="fix : affiche plus détail dans les logs en recette/prod">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768316052474</created>
|
<created>1769077633390</created>
|
||||||
<option name="number" value="00002" />
|
<option name="number" value="00024" />
|
||||||
<option name="presentableId" value="LOCAL-00002" />
|
<option name="presentableId" value="LOCAL-00024" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768316052474</updated>
|
<updated>1769077633390</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00003" summary="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav">
|
<task id="LOCAL-00025" summary="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768316835575</created>
|
<created>1769079030808</created>
|
||||||
<option name="number" value="00003" />
|
<option name="number" value="00025" />
|
||||||
<option name="presentableId" value="LOCAL-00003" />
|
<option name="presentableId" value="LOCAL-00025" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768316835575</updated>
|
<updated>1769079030808</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00004" summary="feat : update du fichier AGENTS.md">
|
<task id="LOCAL-00026" summary="fix : doc de déploiement">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768316965511</created>
|
<created>1769094376813</created>
|
||||||
<option name="number" value="00004" />
|
<option name="number" value="00026" />
|
||||||
<option name="presentableId" value="LOCAL-00004" />
|
<option name="presentableId" value="LOCAL-00026" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768316965511</updated>
|
<updated>1769094376813</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
|
<task id="LOCAL-00027" summary="fix : doc et script de déploiement">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768317786187</created>
|
<created>1769096187792</created>
|
||||||
<option name="number" value="00005" />
|
<option name="number" value="00027" />
|
||||||
<option name="presentableId" value="LOCAL-00005" />
|
<option name="presentableId" value="LOCAL-00027" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768317786187</updated>
|
<updated>1769096187792</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00006" summary="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple">
|
<task id="LOCAL-00028" summary="fix : doc et script de déploiement">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768318875533</created>
|
<created>1769097091268</created>
|
||||||
<option name="number" value="00006" />
|
<option name="number" value="00028" />
|
||||||
<option name="presentableId" value="LOCAL-00006" />
|
<option name="presentableId" value="LOCAL-00028" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768318875533</updated>
|
<updated>1769097091268</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
|
<task id="LOCAL-00029" summary="fix : gitea workflow">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768318921478</created>
|
<created>1769097476629</created>
|
||||||
<option name="number" value="00007" />
|
<option name="number" value="00029" />
|
||||||
<option name="presentableId" value="LOCAL-00007" />
|
<option name="presentableId" value="LOCAL-00029" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768318921478</updated>
|
<updated>1769097476629</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00008" summary="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global">
|
<task id="LOCAL-00030" summary="fix : script de déploiement">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1768498751836</created>
|
<created>1769098182184</created>
|
||||||
<option name="number" value="00008" />
|
<option name="number" value="00030" />
|
||||||
<option name="presentableId" value="LOCAL-00008" />
|
<option name="presentableId" value="LOCAL-00030" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768498751836</updated>
|
<updated>1769098182184</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00009" summary="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
|
<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" />
|
<option name="closed" value="true" />
|
||||||
<created>1768555180530</created>
|
<created>1769098861988</created>
|
||||||
<option name="number" value="00009" />
|
<option name="number" value="00031" />
|
||||||
<option name="presentableId" value="LOCAL-00009" />
|
<option name="presentableId" value="LOCAL-00031" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768555180530</updated>
|
<updated>1769098861988</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="10" />
|
<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>
|
||||||
|
<option name="localTasksCounter" value="72" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -343,6 +735,11 @@
|
|||||||
<entry key="Branch">
|
<entry key="Branch">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
|
<RecentGroup>
|
||||||
|
<option name="FILTER_VALUES">
|
||||||
|
<option value="HEAD" />
|
||||||
|
</option>
|
||||||
|
</RecentGroup>
|
||||||
<RecentGroup>
|
<RecentGroup>
|
||||||
<option name="FILTER_VALUES">
|
<option name="FILTER_VALUES">
|
||||||
<option value="develop" />
|
<option value="develop" />
|
||||||
@@ -363,7 +760,7 @@
|
|||||||
<entry key="branch">
|
<entry key="branch">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
<option value="develop" />
|
<option value="HEAD" />
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
@@ -376,17 +773,63 @@
|
|||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="Feat : (WIP) Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions" />
|
<MESSAGE value="feat : ajout de commentaire" />
|
||||||
<MESSAGE value="Feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)" />
|
<MESSAGE value="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception" />
|
||||||
<MESSAGE value="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)" />
|
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception" />
|
||||||
<MESSAGE value="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception" />
|
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||||
<MESSAGE value="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav" />
|
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||||
<MESSAGE value="feat : update du fichier AGENTS.md" />
|
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||||
<MESSAGE value="feat : update du fichier README.md et CHANGELOG.md" />
|
<MESSAGE value="feat : creer une nouvelle expedtion (WIP)" />
|
||||||
<MESSAGE value="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple" />
|
<MESSAGE value="feat : ajout d'une page de creation d'une expedition" />
|
||||||
<MESSAGE value="test : ajout de TU sur les services et providers" />
|
<MESSAGE value="feat : changelog" />
|
||||||
<MESSAGE value="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global" />
|
<MESSAGE value="feat : lister les expeditions terminees" />
|
||||||
<MESSAGE value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
|
<MESSAGE value="fix: corrections diverses" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
|
<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" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu" />
|
||||||
|
</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>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
46
AGENTS.md
46
AGENTS.md
@@ -1,46 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
Project overview
|
|
||||||
- Symfony 8 + API Platform 4 backend, Nuxt 3 frontend in `frontend/`.
|
|
||||||
- Apache vhost serves API under `/api` and frontend from `frontend/dist`.
|
|
||||||
- API base URL on frontend uses `NUXT_PUBLIC_API_BASE` (see `frontend/.env`).
|
|
||||||
|
|
||||||
Backend conventions
|
|
||||||
- Use English for code identifiers/messages; keep “pont-bascule” as domain term.
|
|
||||||
- API Platform operations are defined on Doctrine entities.
|
|
||||||
- Reception entity is in `src/Entity/Reception.php`, with custom weigh endpoint `/receptions/weigh`.
|
|
||||||
- Reception fields: `date_reception`, `license_plate`, `current_step` (default 0), `is_valid` (default false).
|
|
||||||
- `date_reception` is set by the UI, stored as `DateTimeImmutable`, serialized as `Y-m-d`.
|
|
||||||
- Weight entity (`src/Entity/Weight.php`) is 1–N with Reception, each row stores `type` (`gross` or `tare`), `dsd`, `weight`, `weighed_at` (all nullable except `type`).
|
|
||||||
- Weigh endpoint `/receptions/weigh` returns `PontBasculeReading` with `dsd`, `weight`, `weighedAt` (formatted `Y-m-d`).
|
|
||||||
- Custom exception: `App\Exception\PontBasculeException` with French messages, mapped to 500 in provider.
|
|
||||||
- Parsing of pont-bascule payload is in `src/Service/PontBasculePayloadDecoder.php`.
|
|
||||||
- `config/reference.php` is auto-generated; keep it.
|
|
||||||
|
|
||||||
Frontend conventions
|
|
||||||
- Nuxt SSR disabled; Tailwind used.
|
|
||||||
- Layout in `frontend/layouts/default.vue`: max width `1050px`, header full width.
|
|
||||||
- Tailwind custom color palette is `primary` (e.g. `bg-primary-500`).
|
|
||||||
- API composable in `frontend/composables/useApi.ts` with `get/post/put/patch/delete` and default JSON/PATCH content types.
|
|
||||||
- API errors/success toasts can be customized via `toastErrorMessage`/`toastSuccessMessage` or i18n keys `toastErrorKey`/`toastSuccessKey`. Global method fallbacks use `errors.http.*` keys.
|
|
||||||
- `useApi` uses `useNuxtApp().$i18n` (not `useI18n`) to avoid setup-only constraint in service calls.
|
|
||||||
- Pinia store: `frontend/stores/reception.ts` is the source of truth for the current reception.
|
|
||||||
- Zod is used for form validation (e.g. `frontend/components/reception/reception-form.vue`); shared helpers live in `frontend/utils/zod-errors.ts`.
|
|
||||||
- Weighing logic is shared via `frontend/composables/useWeighing.ts`.
|
|
||||||
- Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`.
|
|
||||||
- Active nav styles in header use `NuxtLink` with `custom` slot.
|
|
||||||
- Reusable UI components live under `frontend/components/ui/` and are auto-imported with `Ui` prefix (e.g. `UiLoadingDots`).
|
|
||||||
- Service layer lives in `frontend/services/` with typed DTOs in `frontend/services/dto/`.
|
|
||||||
- Reception service uses `receptions`, `receptions/{id}`, `receptions/weigh` and supports success/error toast keys.
|
|
||||||
- Reception receipt endpoint is `receptions/{id}/receipt` (PDF) via `frontend/composables/usePdfPrinter.ts`.
|
|
||||||
|
|
||||||
Environment & routing
|
|
||||||
- Frontend dev server: `npm run dev` in `frontend/`.
|
|
||||||
- API base for local dev: `http://localhost:8080/api` (set in `frontend/.env` via `NUXT_PUBLIC_API_BASE`).
|
|
||||||
- CORS handled by Nelmio; `.env` includes `CORS_ALLOW_ORIGIN` regex for localhost.
|
|
||||||
- Nuxt i18n locales live in `frontend/i18n/locales` (configured via `langDir: 'locales'`).
|
|
||||||
- Default locale is `fr`; translations in `frontend/i18n/locales/fr.json`.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
- Do not add a GET that creates resources; use POST + PATCH.
|
|
||||||
- Keep endpoints in plural (API Platform convention).
|
|
||||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -9,13 +9,57 @@ Ajouter dans le fichier .env
|
|||||||
- DATABASE_URL
|
- DATABASE_URL
|
||||||
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||||
- PONT_BASCULE_URL
|
- 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
|
Ajouter dans le fichier .env du frontend
|
||||||
- NUXT_PUBLIC_API_BASE
|
- NUXT_PUBLIC_API_BASE
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
* [#203] Réceptions — Parcours de pesée multi-étapes (début)
|
* [#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
|
### Changed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
127
CLAUDE.md
Normal file
127
CLAUDE.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# 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
|
||||||
|
├── composables/ # useApi, useWeighing, usePdfPrinter, useAppVersion
|
||||||
|
├── services/ # Couche service avec DTOs typés dans services/dto/
|
||||||
|
├── stores/ # Pinia stores (reception, shipment, auth)
|
||||||
|
├── 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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## Domaine métier clé
|
||||||
|
|
||||||
|
### Réception (pesée pont-bascule)
|
||||||
|
- Entité principale `Reception` : `date_reception` (DateTimeImmutable, format `Y-m-d`), `identification_number` (auto `N-BR-####`), `current_step` (défaut 0), `is_valid` (défaut false).
|
||||||
|
- `Weight` (1-N avec Reception) : `type` (`gross`/`tare`), `dsd`, `weight`, `weighed_at`.
|
||||||
|
- Endpoint pesée : `/receptions/weigh` → `PontBasculeReading` (dsd, weight, weighedAt).
|
||||||
|
- 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`.
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
### 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), `Truck`, `Carrier`, `Driver`, `Vehicle`.
|
||||||
|
- `Address` expose `fullAddress` via getter.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- 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`
|
||||||
67
README.md
67
README.md
@@ -21,13 +21,17 @@ Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifi
|
|||||||
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
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 :
|
Vérifier que dans le .env.local, vous avez :
|
||||||
* APP_SECRET (doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
* 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
|
* 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_BYPASS (doit être à true en dev)
|
||||||
* PONT_BASCULE_URL
|
* 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 :
|
Vérifier que dans le .env du dossier frontend, vous avez :
|
||||||
* NUXT_PUBLIC_API_BASE
|
* NUXT_PUBLIC_API_BASE="http://localhost:8080/api"
|
||||||
|
|
||||||
### Configuration xdebug
|
### Configuration xdebug
|
||||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||||
@@ -54,6 +58,45 @@ 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
|
## Commandes utiles
|
||||||
Pour restart le container
|
Pour restart le container
|
||||||
```bash
|
```bash
|
||||||
@@ -71,3 +114,21 @@ Pour clear le cache Symfony
|
|||||||
```bash
|
```bash
|
||||||
make cache-clear
|
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
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
"dompdf/dompdf": "^3.1",
|
"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",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "8.0.*",
|
"symfony/framework-bundle": "8.0.*",
|
||||||
"symfony/http-client": "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.*",
|
||||||
@@ -85,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1403
composer.lock
generated
1403
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],
|
||||||
];
|
];
|
||||||
|
|||||||
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
|
||||||
1457
config/reference.php
1457
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:
|
||||||
|
|||||||
2
config/version.yaml
Normal file
2
config/version.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
parameters:
|
||||||
|
app.version: '0.0.70'
|
||||||
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,32 +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
|
|
||||||
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteRule ^index\.php$ - [L]
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^ /api/index.php [L]
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
AliasMatch "^/(?!api)(.*)$" "/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>
|
|
||||||
@@ -3,3 +3,11 @@
|
|||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { load } = useAppVersion()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
9
frontend/assets/css/main.css
Normal file
9
frontend/assets/css/main.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
frontend/components/address.vue
Normal file
101
frontend/components/address.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<form @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-label" v-model="form.label" label="Libellé" />
|
||||||
|
<UiTextInput id="address-street" v-model="form.street" label="Rue" />
|
||||||
|
<UiTextInput id="address-street2" v-model="form.street2" label="Complément" />
|
||||||
|
<UiTextInput id="address-postalCode" v-model="form.postalCode" label="Code postal" />
|
||||||
|
<UiTextInput id="address-city" v-model="form.city" label="Ville" />
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AddressPayload } from "~/services/address"
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type?: "supplier" | "customer",
|
||||||
|
address?: AddressPayload | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const emptyForm = (): AddressPayload => ({
|
||||||
|
label: "",
|
||||||
|
street: "",
|
||||||
|
street2: null,
|
||||||
|
postalCode: "",
|
||||||
|
city: "",
|
||||||
|
countryCode: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
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.label = data.label ?? ""
|
||||||
|
form.street = data.street ?? ""
|
||||||
|
form.street2 = data.street2 ?? null
|
||||||
|
form.postalCode = data.postalCode ?? ""
|
||||||
|
form.city = data.city ?? ""
|
||||||
|
form.countryCode = data.countryCode ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.address,
|
||||||
|
(addr) => {
|
||||||
|
hydrateForm(addr)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
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>
|
||||||
62
frontend/components/commun/update-weight.vue
Normal file
62
frontend/components/commun/update-weight.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiDateInput
|
||||||
|
label="Date de pesée"
|
||||||
|
v-model="localWeight.weighedAt"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiNumberInput
|
||||||
|
label="Dsd"
|
||||||
|
class="col-start-2"
|
||||||
|
labelClass="font-bold uppercase"
|
||||||
|
v-model="localWeight.dsd"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
wrapper-class="flex-col"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
191
frontend/components/reception/reception-bovine-received.vue
Normal file
191
frontend/components/reception/reception-bovine-received.vue
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
||||||
|
class="flex flex-col gap-16">
|
||||||
|
<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>
|
||||||
|
<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="goNext"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO Ajouter un composable pour le toaster qui gère les key i18n
|
||||||
|
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>
|
||||||
@@ -1,67 +1,514 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-1 items-start gap-8 mb-16">
|
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
||||||
<h1 class="font-bold text-5xl uppercase">Réception</h1>
|
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Réception</h1>
|
||||||
<div>
|
<!-- Nom de l'utilisateur -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Date de réception -->
|
||||||
|
<UiDateInput
|
||||||
|
id="reception-date"
|
||||||
|
v-model="form.receptionDate"
|
||||||
|
label="Date de réception"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- Type de réception -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Fournisseur -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Adresse fournisseur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
label="Adresse"
|
||||||
|
:options="supplierAddresses.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))"
|
||||||
|
:disabled="isLoadingSuppliers || supplierAddresses.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
<!-- Camion -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Transporteur -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Plaque d'immatriculation -->
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<!-- Immatriculation (LIOT) -->
|
||||||
<label for="reception-date" class="font-bold uppercase text-xl mb-4">Date de reception</label>
|
<UiSelect
|
||||||
<input
|
v-if="isLiotCarrier"
|
||||||
id="reception-date"
|
id="reception-vehicle"
|
||||||
v-model="form.receptionDate"
|
v-model="form.vehicleId"
|
||||||
type="date"
|
label="Immatriculation"
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
/>
|
value: String(vehicle.id),
|
||||||
</div>
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4 h-[64px]"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
>Peser
|
>Valider
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
import {useReceptionStore} from '~/stores/reception'
|
||||||
|
import type {ReceptionTypeData} from '~/services/dto/reception-type-data'
|
||||||
type ReceptionFormData = {
|
import {getReceptionTypeList} from '~/services/reception-type'
|
||||||
licensePlate: string
|
import type {UserData} from '~/services/dto/user-data'
|
||||||
receptionDate: string
|
import {getUsers} from '~/services/auth'
|
||||||
}
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
import type {SupplierData} from '~/services/dto/supplier-data'
|
||||||
|
import {getSupplierList} from '~/services/supplier'
|
||||||
|
import type {TruckData} from '~/services/dto/truck-data'
|
||||||
|
import {getTruckList} from '~/services/truck'
|
||||||
|
import type {CarrierData} from '~/services/dto/carrier-data'
|
||||||
|
import {getCarrierList} from '~/services/carrier'
|
||||||
|
import type {DriverData} from '~/services/dto/driver-data'
|
||||||
|
import {getDriverList} from '~/services/driver'
|
||||||
|
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||||
|
import {getVehicleList} from '~/services/vehicle'
|
||||||
|
import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
|
||||||
|
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
||||||
|
import type {ReceptionFormData} from "~/services/dto/reception-data";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const form = reactive<ReceptionFormData>({
|
const form = reactive<ReceptionFormData>({
|
||||||
licensePlate: '',
|
licensePlate: '',
|
||||||
receptionDate: new Date().toISOString().slice(0, 10)
|
receptionDate: new Date().toISOString().slice(0, 10),
|
||||||
|
receptionTypeId: '',
|
||||||
|
userId: '',
|
||||||
|
supplierId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: ''
|
||||||
})
|
})
|
||||||
const allowAnyLicensePlate = ref(false)
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const suppliers = ref<SupplierData[]>([])
|
||||||
|
const isLoadingSuppliers = ref(false)
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
// Empêche les watchers de reset des champs pendant le remplissage initial
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
|
||||||
|
// Transporteur sélectionné dans le formulaire
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
// Indique si le transporteur est LIOT
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
// Adresses disponibles pour le fournisseur sélectionné
|
||||||
|
const supplierAddresses = computed(() => {
|
||||||
|
const supplierId = Number(form.supplierId)
|
||||||
|
if (!Number.isFinite(supplierId)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? []
|
||||||
|
})
|
||||||
|
// Chauffeurs filtrés par transporteur (LIOT)
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
// Véhicules filtrés par transporteur + type de camion
|
||||||
|
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 selectedReceptionType = computed(() =>
|
||||||
|
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||||
|
)
|
||||||
|
|
||||||
|
// Supprime les données bovines si on change de type de réception
|
||||||
|
const clearReceptionBovines = async (receptionIri: string) => {
|
||||||
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
|
for (const selection of existing) {
|
||||||
|
await deleteReceptionBovine(selection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydrate le formulaire depuis la réception en cours
|
||||||
watch(
|
watch(
|
||||||
() => receptionStore.current,
|
() => receptionStore.current,
|
||||||
(reception) => {
|
(reception) => {
|
||||||
|
isHydrating.value = true
|
||||||
form.licensePlate = reception?.licensePlate ?? ''
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
|
form.receptionDate = reception?.receptionDate ?? 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 }
|
{immediate: true}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Charge la liste des users pour le select
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charge la liste des fournisseurs pour le select
|
||||||
|
const loadSuppliers = async () => {
|
||||||
|
isLoadingSuppliers.value = true
|
||||||
|
try {
|
||||||
|
suppliers.value = await getSupplierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingSuppliers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charge la liste des camions pour le select
|
||||||
|
const loadTrucks = async () => {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charge la liste des transporteurs pour le select
|
||||||
|
const loadCarriers = async () => {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charge la liste des chauffeurs pour le select
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charge la liste des véhicules pour le select
|
||||||
|
const loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On met le user connecté par défaut dans le select
|
||||||
|
const setDefaultUser = () => {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On récupère toutes les données des selects au chargement du composant
|
||||||
|
onMounted(async () => {
|
||||||
|
receptionTypes.value = await getReceptionTypeList()
|
||||||
|
await loadUsers()
|
||||||
|
await loadSuppliers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadDrivers()
|
||||||
|
await loadVehicles()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.supplierId, form.addressId, suppliers.value],
|
||||||
|
() => {
|
||||||
|
if (!form.supplierId) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && supplierAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(supplierAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = supplierAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (supplierAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(supplierAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Valide le formulaire et crée/met à jour la réception
|
||||||
async function validate() {
|
async function validate() {
|
||||||
const normalizedLicensePlate = form.licensePlate.trim()
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
const normalizedReceptionDate = form.receptionDate.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) {
|
if (!receptionStore.current) {
|
||||||
const created = await receptionStore.createReception({
|
const created = await receptionStore.createReception({
|
||||||
currentStep: 1,
|
currentStep: 1,
|
||||||
licensePlate: normalizedLicensePlate,
|
...payload
|
||||||
receptionDate: normalizedReceptionDate
|
|
||||||
})
|
})
|
||||||
if (created) {
|
if (created) {
|
||||||
await router.push(`/reception/${created.id}`)
|
await router.push(`/reception/${created.id}`)
|
||||||
@@ -69,11 +516,20 @@ async function validate() {
|
|||||||
return
|
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
|
const nextStep = receptionStore.current.currentStep + 1
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
currentStep: nextStep,
|
currentStep: nextStep,
|
||||||
licensePlate: normalizedLicensePlate,
|
...payload
|
||||||
receptionDate: normalizedReceptionDate
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
258
frontend/components/reception/reception-product-received.vue
Normal file
258
frontend/components/reception/reception-product-received.vue
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center gap-16">
|
||||||
|
<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]"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
|
class="flex gap-4 w-[550px] 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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="goNext"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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('')
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col items-center mt-[164px] gap-32">
|
|
||||||
<div class="flex gap-8 items-center justify-center">
|
|
||||||
<!--@TODO Prendre en compte que l'on peut aussi décharger de la marchandise-->
|
|
||||||
<h1 class="text-4xl uppercase font-bold">Décharger les bêtes</h1>
|
|
||||||
<UiLoadingDots />
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
|
||||||
@click="goNext"
|
|
||||||
>Peser</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
|
||||||
|
|
||||||
const receptionStore = useReceptionStore()
|
|
||||||
|
|
||||||
async function goNext() {
|
|
||||||
if (!receptionStore.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = receptionStore.current.currentStep + 1
|
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
currentStep: nextStep
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,50 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1>
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
<p class="text-primary-500 uppercase text-2xl text-primary-500 mt-2">Pont-bascule connecté</p>
|
||||||
<div
|
<div
|
||||||
v-if="showLoadingBox"
|
v-if="!displayWeight"
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
<UiLoadingDots />
|
<UiLoadingDots />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="displayWeight !== null" class="w-full">
|
<div v-else class="w-full">
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
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
|
{{ displayWeight }} kg
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="grid grid-cols-2 border border-black text-center">-->
|
|
||||||
<!-- <p class="border-r border-black py-3 text-4xl font-bold">DSD</p>-->
|
|
||||||
<!-- <p class="py-3 text-4xl">{{ displayDsd }}</p>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center mt-[54px]">
|
<div class="flex justify-center mt-[54px]">
|
||||||
<button
|
<UiButton
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
@click="fetchWeight"
|
@click="fetchWeight"
|
||||||
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="displayWeight !== null && !showGenerateReceipt"
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="saveWeight"
|
@click="saveWeight"
|
||||||
>Valider la pesée</button>
|
>Valider la pesée</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="showGenerateReceipt"
|
v-if="showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="printReceipt"
|
@click="printReceipt"
|
||||||
>Générer le bon</button>
|
>Générer le bon</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<UiPdfPrinter ref="pdfPrinter" />
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import {computed} from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useWeighing } from '~/composables/useWeighing'
|
import { useWeighing } from '~/composables/useWeighing'
|
||||||
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -54,14 +49,9 @@ const props = defineProps<{
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const { current: storeReception } = storeToRefs(receptionStore)
|
const { current: storeReception } = storeToRefs(receptionStore)
|
||||||
type PdfPrinterHandle = {
|
const { printPdf } = usePdfPrinter()
|
||||||
print: (url: string) => Promise<void>
|
|
||||||
}
|
|
||||||
// Ref sur le composant d'impression pour déclencher le print() du PDF.
|
|
||||||
const pdfPrinter = ref<PdfPrinterHandle | null>(null)
|
|
||||||
const {
|
const {
|
||||||
displayWeight,
|
displayWeight,
|
||||||
displayDsd,
|
|
||||||
title,
|
title,
|
||||||
showLoadingBox,
|
showLoadingBox,
|
||||||
fetchWeight,
|
fetchWeight,
|
||||||
@@ -72,17 +62,21 @@ const {
|
|||||||
updateReception: receptionStore.updateReception,
|
updateReception: receptionStore.updateReception,
|
||||||
loadReception: receptionStore.loadReception
|
loadReception: receptionStore.loadReception
|
||||||
})
|
})
|
||||||
|
// Affiche le bouton de génération du bon à l'étape tare
|
||||||
const showGenerateReceipt = computed(
|
const showGenerateReceipt = computed(
|
||||||
() => props.mode === 'tare' && displayWeight.value !== null
|
() => props.mode === 'tare' && displayWeight.value !== null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Génère le bon de réception, puis clôture la réception
|
||||||
const printReceipt = async () => {
|
const printReceipt = async () => {
|
||||||
if (!import.meta.client || !receptionStore.current || !pdfPrinter.value) {
|
if (!import.meta.client || !receptionStore.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveWeight()
|
await saveWeight()
|
||||||
await pdfPrinter.value.print(`/receptions/${receptionStore.current.id}/receipt`)
|
const reception = receptionStore.current
|
||||||
|
const filename = `${reception.identificationNumber ?? reception.id}_${reception.supplier?.name ?? 'fournisseur'}_${reception.licensePlate ?? 'immat'}.pdf`
|
||||||
|
await printPdf(`/receptions/${reception.id}/receipt`, filename)
|
||||||
|
|
||||||
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
@@ -98,7 +92,4 @@ const printReceipt = async () => {
|
|||||||
await router.push('/')
|
await router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchWeight()
|
|
||||||
})
|
|
||||||
</script>
|
</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>
|
||||||
269
frontend/components/reception/update-merchandise.vue
Normal file
269
frontend/components/reception/update-merchandise.vue
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-full col-start-1 row-start-1">
|
||||||
|
<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-[336px_336px_355px_200px] w-[160px_160px_200px_180px] mt-9 mb-7"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full grid grid-cols-[3fr_1fr] gap-12 col-start-2 row-start-1">
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
|
class="flex gap-[218px]"
|
||||||
|
>
|
||||||
|
<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="text-xl uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && isAutres"
|
||||||
|
class="flex flex-col justify-self-end max-w-[182px]"
|
||||||
|
>
|
||||||
|
<UiTextInput
|
||||||
|
id="merchandise-detail"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
v-model="merchandiseDetail"
|
||||||
|
placeholder="Préciser"
|
||||||
|
:maxlength="255"
|
||||||
|
class="h-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && isGranule"
|
||||||
|
class="flex flex-col gap-10 w-full col-start-2 row-start-1"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-[max-content_max-content_max-content_max-content] 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>
|
||||||
554
frontend/components/shipment/shipment-form.vue
Normal file
554
frontend/components/shipment/shipment-form.vue
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
<template>
|
||||||
|
<form @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>
|
||||||
|
<!-- Nom de l'utilisateur -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Date de l'éxpedition -->
|
||||||
|
<UiDateInput
|
||||||
|
id="shipment-date"
|
||||||
|
v-model="form.shipmentDate"
|
||||||
|
label="Date du jour"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- Type d'expédition -->
|
||||||
|
<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
|
||||||
|
}))"
|
||||||
|
/>
|
||||||
|
<UiNumberInput
|
||||||
|
id="shipment-type-quantity"
|
||||||
|
v-model="shipmentQuantity"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="1200"
|
||||||
|
:disabled="!selectedShipmentTypeId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Client -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Adresse du client -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
:options="customerAddressOptions"
|
||||||
|
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||||
|
label="Adresse"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
<!-- Camion -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Transporteur -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Plaque d'immatriculation (hors LIOT) -->
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Immatriculation (LIOT) -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {UserData} from '~/services/dto/user-data'
|
||||||
|
import type {CustomerData} from '~/services/dto/customer-data'
|
||||||
|
import type {TruckData} from '~/services/dto/truck-data'
|
||||||
|
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 type {AddressData} from '~/services/dto/address-data'
|
||||||
|
import {getUsers} from '~/services/auth'
|
||||||
|
import {getCustomerList} from '~/services/customer'
|
||||||
|
import {getTruckList} from '~/services/truck'
|
||||||
|
import {getCarrierList} from '~/services/carrier'
|
||||||
|
import {getVehicleList} from '~/services/vehicle'
|
||||||
|
import {getDriverList} from '~/services/driver'
|
||||||
|
import type {ShipmentFormData} from '~/services/dto/shipment-data'
|
||||||
|
import {SUPPLIER_CODE} from "~/utils/constants"
|
||||||
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
import {useShipmentStore} from '~/stores/shipment'
|
||||||
|
import {computed, reactive, ref, watch, onMounted} from 'vue'
|
||||||
|
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
|
||||||
|
import {getShipmentTypeList} from "~/services/shipment-type";
|
||||||
|
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const customers = ref<CustomerData[]>([])
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingShipmentTypes = ref(false)
|
||||||
|
const isLoadingCustomers = ref(false)
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
|
const selectedShipmentTypeId = ref('')
|
||||||
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
|
// Transporteur sélectionné dans le formulaire
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
|
||||||
|
const form = reactive<ShipmentFormData>({
|
||||||
|
userId: '',
|
||||||
|
shipmentDate: new Date().toISOString().slice(0, 10),
|
||||||
|
customerId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: '',
|
||||||
|
licensePlate: '',
|
||||||
|
})
|
||||||
|
// Adresses liées au client sélectionné
|
||||||
|
const customerAddresses = computed<AddressData[]>(() => {
|
||||||
|
const customerId = Number(form.customerId)
|
||||||
|
if (!Number.isFinite(customerId)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return customers.value.find((customer) => customer.id === customerId)?.addresses ?? []
|
||||||
|
})
|
||||||
|
// Options pour le select des adresses du client
|
||||||
|
const customerAddressOptions = computed(() =>
|
||||||
|
customerAddresses.value
|
||||||
|
.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
// Chauffeurs liés au transporteur sélectionné (LIOT)
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
// Véhicules liés au transporteur + camion sélectionnés (LIOT)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// Chargement des données pour les selects
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadShipmentType = async () => {
|
||||||
|
isLoadingShipmentTypes.value = true
|
||||||
|
try {
|
||||||
|
bovineShipment.value = await getShipmentTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingShipmentTypes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCustomers = async () => {
|
||||||
|
isLoadingCustomers.value = true
|
||||||
|
try {
|
||||||
|
customers.value = await getCustomerList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCustomers.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 loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// On met le user connecté par défaut dans le select
|
||||||
|
const setDefaultUser = () => {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Chargement initial des données
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadShipmentType()
|
||||||
|
await loadUsers()
|
||||||
|
await loadCustomers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadVehicles()
|
||||||
|
await loadDrivers()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
})
|
||||||
|
// Hydrate le formulaire depuis l'expédition en cours
|
||||||
|
watch(
|
||||||
|
() => shipmentStore.current,
|
||||||
|
(shipment) => {
|
||||||
|
isHydrating.value = true
|
||||||
|
form.licensePlate = shipment?.licensePlate ?? ''
|
||||||
|
form.shipmentDate = shipment?.shipmentDate ?? 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}
|
||||||
|
)
|
||||||
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.customerId, form.addressId, customers.value],
|
||||||
|
() => {
|
||||||
|
if (!form.customerId) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = customerAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
||||||
|
const applyLiotDefaults = () => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
applyLiotDefaults()
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => isHydrating.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) {
|
||||||
|
applyLiotDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, form.vehicleId, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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})
|
||||||
|
// Valide le formulaire et crée/met à jour l'expédition
|
||||||
|
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>
|
||||||
95
frontend/components/shipment/shipment-weight.vue
Normal file
95
frontend/components/shipment/shipment-weight.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<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>
|
||||||
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
|
<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-primary-500 h-[90px] mt-12 mb-[86px] 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 { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useWeighingShipment } from '~/composables/useWeighing'
|
||||||
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
|
import { useShipmentStore } from '~/stores/shipment'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const { current: storeShipment } = storeToRefs(shipmentStore)
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
const {
|
||||||
|
displayWeight,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
} = useWeighingShipment({
|
||||||
|
modeShipment: props.mode,
|
||||||
|
shipment: storeShipment,
|
||||||
|
updateShipment: shipmentStore.updateShipment,
|
||||||
|
loadShipment: shipmentStore.loadShipment
|
||||||
|
})
|
||||||
|
// Affiche le bouton de génération du bon à l'étape tare
|
||||||
|
const showGenerateReceipt = computed(
|
||||||
|
() => props.mode === 'tare' && displayWeight.value !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
// Génère le bon d'expédition, puis clôture l'expédition
|
||||||
|
const printReceipt = async () => {
|
||||||
|
if (!import.meta.client || !shipmentStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWeight()
|
||||||
|
const shipment = shipmentStore.current
|
||||||
|
const filename = `${shipment.identificationNumber ?? shipment.id}_${shipment.customer?.label ?? 'client'}_${shipment.licensePlate ?? 'immat'}.pdf`
|
||||||
|
await printPdf(`/shipments/${shipment.id}/receipt`, filename)
|
||||||
|
|
||||||
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
const result = await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
isValid: true
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shipmentStore.clearCurrent()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
</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>
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label :for="inputId" class="font-bold uppercase text-xl mb-4">{{ label }}</label>
|
<label :for="inputId" class="font-bold uppercase text-xl text-primary-500">{{ label }}</label>
|
||||||
<input
|
<div class="flex items-end gap-8">
|
||||||
:id="inputId"
|
<input
|
||||||
:value="modelValue"
|
:id="inputId"
|
||||||
v-maska="maskOptions"
|
:value="modelValue"
|
||||||
type="text"
|
v-maska="maskOptions"
|
||||||
:maxlength="maxLength"
|
type="text"
|
||||||
:placeholder="placeholderText"
|
:maxlength="maxLength"
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
:placeholder="placeholderText"
|
||||||
@input="handleInput"
|
class="border-b border-black flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
|
||||||
/>
|
@input="handleInput"
|
||||||
<label :for="checkboxId" class="mt-3 flex items-center gap-3 text-sm">
|
/>
|
||||||
<input
|
<UiCheckbox
|
||||||
:id="checkboxId"
|
:id="checkboxId"
|
||||||
:checked="allowAny"
|
:model-value="allowAny"
|
||||||
type="checkbox"
|
label="Autoriser un format libre"
|
||||||
class="h-4 w-4 accent-primary-500"
|
wrapper-class="ml-auto"
|
||||||
@change="toggleAllowAny"
|
label-class="gap-3 whitespace-nowrap text-sm"
|
||||||
/>
|
input-class="h-4 w-4 accent-primary-500"
|
||||||
Autoriser un format libre
|
@update:modelValue="handleAllowAnyChange"
|
||||||
</label>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -78,13 +79,7 @@ const handleInput = (event: Event) => {
|
|||||||
emit('update:modelValue', target.value)
|
emit('update:modelValue', target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleAllowAny = (event: Event) => {
|
const handleAllowAnyChange = (nextValue: boolean) => {
|
||||||
const target = event.target as HTMLInputElement | null
|
|
||||||
if (!target) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextValue = target.checked
|
|
||||||
emit('update:allowAny', nextValue)
|
emit('update:allowAny', nextValue)
|
||||||
if (!nextValue) {
|
if (!nextValue) {
|
||||||
emit('update:modelValue', props.modelValue)
|
emit('update:modelValue', props.modelValue)
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<iframe ref="printFrame" class="hidden" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
|
||||||
|
|
||||||
const printFrame = ref<HTMLIFrameElement | null>(null)
|
|
||||||
const { printPdf } = usePdfPrinter()
|
|
||||||
|
|
||||||
// Expose une methode simple pour imprimer un PDF depuis les ecrans.
|
|
||||||
const print = async (url: string): Promise<void> => {
|
|
||||||
return printPdf(url, printFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
print
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
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>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { FetchOptions } from 'ofetch'
|
import type { FetchOptions } from 'ofetch'
|
||||||
import { $fetch, FetchError } from 'ofetch'
|
import { $fetch, FetchError } from 'ofetch'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
export type AnyObject = Record<string, unknown>
|
export type AnyObject = Record<string, unknown>
|
||||||
|
|
||||||
@@ -26,7 +27,9 @@ export const useApi = (): ApiClient => {
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const baseURL = config.public.apiBase ?? '/api'
|
const baseURL = config.public.apiBase ?? '/api'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const auth = useAuthStore()
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
|
let isHandlingUnauthorized = false
|
||||||
const i18n = nuxtApp.$i18n as
|
const i18n = nuxtApp.$i18n as
|
||||||
| {
|
| {
|
||||||
t: (key: string) => string
|
t: (key: string) => string
|
||||||
@@ -70,12 +73,17 @@ export const useApi = (): ApiClient => {
|
|||||||
const client = $fetch.create({
|
const client = $fetch.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
retry: 0,
|
retry: 0,
|
||||||
onResponse({ options }) {
|
credentials: 'include',
|
||||||
|
onResponse({ options, response }) {
|
||||||
const apiOptions = options as ApiFetchOptions<'json'>
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
if (apiOptions?.toast === false) {
|
if (apiOptions?.toast === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response?.status && response.status >= 400) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const successKey = apiOptions?.toastSuccessKey
|
const successKey = apiOptions?.toastSuccessKey
|
||||||
const successMessage =
|
const successMessage =
|
||||||
apiOptions?.toastSuccessMessage ||
|
apiOptions?.toastSuccessMessage ||
|
||||||
@@ -88,7 +96,23 @@ export const useApi = (): ApiClient => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResponseError({ response, error, options }) {
|
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'>
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
if (apiOptions?.toast === false) {
|
if (apiOptions?.toast === false) {
|
||||||
return
|
return
|
||||||
|
|||||||
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 }
|
||||||
|
}
|
||||||
@@ -1,40 +1,29 @@
|
|||||||
import type { Ref } from 'vue'
|
|
||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
|
|
||||||
type PrintFrameRef = Ref<HTMLIFrameElement | null>
|
|
||||||
|
|
||||||
export const usePdfPrinter = () => {
|
export const usePdfPrinter = () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const printPdf = async (url: string, frameRef: PrintFrameRef): Promise<void> => {
|
const printPdf = async (url: string, filename = 'document.pdf'): Promise<void> => {
|
||||||
if (!import.meta.client) {
|
const blob = await api.getBlob(url)
|
||||||
return
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
const frame = frameRef.value
|
return {
|
||||||
if (!frame) {
|
printPdf
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// On charge le PDF en blob pour rester en same-origin dans l'iframe.
|
|
||||||
const blob = await api.getBlob(url)
|
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
const tryPrint = () => {
|
|
||||||
frame.contentWindow?.focus()
|
|
||||||
frame.contentWindow?.print()
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.onload = () => {
|
|
||||||
tryPrint()
|
|
||||||
// On libere l'URL blob apres l'impression.
|
|
||||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 2000)
|
|
||||||
}
|
|
||||||
frame.src = blobUrl
|
|
||||||
setTimeout(tryPrint, 1200)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
printPdf
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import type {Ref} from 'vue'
|
|
||||||
import {computed, ref} from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
import type {ReceptionData, WeightEntryData} from '~/services/dto/reception-data'
|
import type {WeightEntryData} from '~/services/dto/reception-data'
|
||||||
import type {WeightData} from '~/services/dto/weight-data'
|
import type {WeightData} from '~/services/dto/weight-data'
|
||||||
import {getWeight} from '~/services/reception'
|
import {getWeight} from '~/services/reception'
|
||||||
|
import {getWeightShipment} from '~/services/shipment'
|
||||||
import {createWeight, updateWeight} from '~/services/weight'
|
import {createWeight, updateWeight} from '~/services/weight'
|
||||||
|
import type {UseWeighingShipmentOptions, UseWeighingOptions} from '~/services/weight'
|
||||||
|
import type {WeightShipmentEntryData} from "~/services/dto/shipment-data";
|
||||||
|
|
||||||
export type WeighingMode = 'gross' | 'tare'
|
export type WeighingMode = 'gross' | 'tare'
|
||||||
|
|
||||||
type UseWeighingOptions = {
|
|
||||||
mode: WeighingMode
|
|
||||||
reception: Ref<ReceptionData | null>
|
|
||||||
updateReception: (id: number, payload: Partial<ReceptionData>) => Promise<ReceptionData | null>
|
|
||||||
loadReception?: (id: number) => Promise<ReceptionData | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWeighing = ({
|
export const useWeighing = ({
|
||||||
mode,
|
mode,
|
||||||
reception,
|
reception,
|
||||||
updateReception,
|
updateReception,
|
||||||
loadReception
|
loadReception
|
||||||
}: UseWeighingOptions) => {
|
}: UseWeighingOptions) => {
|
||||||
const weightData = ref<WeightData | null>(null)
|
const weightData = ref<WeightData | null>(null)
|
||||||
const isFetching = ref(false)
|
const isFetching = ref(false)
|
||||||
|
|
||||||
@@ -31,9 +27,7 @@ export const useWeighing = ({
|
|||||||
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
||||||
const showLoadingBox = computed(
|
const showLoadingBox = computed(() => isFetching.value)
|
||||||
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
|
||||||
)
|
|
||||||
|
|
||||||
const fetchWeight = async () => {
|
const fetchWeight = async () => {
|
||||||
isFetching.value = true
|
isFetching.value = true
|
||||||
@@ -65,7 +59,7 @@ export const useWeighing = ({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await createWeight({
|
await createWeight({
|
||||||
reception: `/receptions/${reception.value.id}`,
|
reception: `/api/receptions/${reception.value.id}`,
|
||||||
type: mode,
|
type: mode,
|
||||||
dsd: baseDsd,
|
dsd: baseDsd,
|
||||||
weight: baseWeight,
|
weight: baseWeight,
|
||||||
@@ -97,3 +91,85 @@ export const useWeighing = ({
|
|||||||
saveWeight
|
saveWeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useWeighingShipment = ({
|
||||||
|
modeShipment,
|
||||||
|
shipment,
|
||||||
|
updateShipment,
|
||||||
|
loadShipment
|
||||||
|
}: UseWeighingShipmentOptions) => {
|
||||||
|
const weightData = ref<WeightData | null>(null)
|
||||||
|
const isFetching = ref(false)
|
||||||
|
|
||||||
|
const currentWeightEntry = computed<WeightShipmentEntryData | null>(() => {
|
||||||
|
const weights = shipment.value?.weights ?? []
|
||||||
|
return weights.find((entry) => entry.type === modeShipment) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
|
const title = computed(() => (modeShipment === 'gross' ? 'Pesée à vide' : 'Pesée à plein'))
|
||||||
|
const showLoadingBox = computed(() => isFetching.value)
|
||||||
|
|
||||||
|
const fetchWeight = async () => {
|
||||||
|
isFetching.value = true
|
||||||
|
weightData.value = await getWeightShipment().finally(() => {
|
||||||
|
isFetching.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveWeight = async () => {
|
||||||
|
if (!shipment.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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEntry?.id) {
|
||||||
|
await updateWeight(existingEntry.id, {
|
||||||
|
type: modeShipment,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createWeight({
|
||||||
|
shipment: `/api/shipments/${shipment.value.id}`,
|
||||||
|
type: modeShipment,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = modeShipment === 'tare'
|
||||||
|
? shipment.value.currentStep
|
||||||
|
: shipment.value.currentStep + 1
|
||||||
|
await updateShipment(shipment.value.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
isValid: shipment.value.isValid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loadShipment) {
|
||||||
|
await loadShipment(shipment.value.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
22
frontend/constants/steps.ts
Normal file
22
frontend/constants/steps.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export enum StepLabel {
|
||||||
|
Reception = 'Réception',
|
||||||
|
GrossWeighing = 'Pesée à plein',
|
||||||
|
Selection = 'Sélection réception',
|
||||||
|
TareWeighing = 'Pesée à vide',
|
||||||
|
Shipment = 'Expédition',
|
||||||
|
ShipmentLoading = 'Chargement',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RECEPTION_STEP_LABELS = [
|
||||||
|
StepLabel.Reception,
|
||||||
|
StepLabel.GrossWeighing,
|
||||||
|
StepLabel.Selection,
|
||||||
|
StepLabel.TareWeighing
|
||||||
|
]
|
||||||
|
|
||||||
|
export const SHIPMENT_STEP_LABELS = [
|
||||||
|
StepLabel.Shipment,
|
||||||
|
StepLabel.TareWeighing,
|
||||||
|
StepLabel.ShipmentLoading,
|
||||||
|
StepLabel.GrossWeighing,
|
||||||
|
]
|
||||||
@@ -1,23 +1,149 @@
|
|||||||
{
|
{
|
||||||
"errors": {
|
"errors": {
|
||||||
"http": {
|
"http": {
|
||||||
"get": "Impossible de récupérer les données.",
|
"get": "Impossible de récupérer les données.",
|
||||||
"post": "Impossible de créer la ressource.",
|
"post": "Impossible de créer la ressource.",
|
||||||
"put": "Impossible de mettre à jour la ressource.",
|
"put": "Impossible de mettre à jour la ressource.",
|
||||||
"patch": "Impossible de mettre à jour la ressource.",
|
"patch": "Impossible de mettre à jour la ressource.",
|
||||||
"delete": "Impossible de supprimer 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.",
|
||||||
|
"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.",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"reception": {
|
"success": {
|
||||||
"list": "Impossible de récupérer la liste des réceptions.",
|
"reception": {
|
||||||
"fetch": "Impossible de récupérer la réception.",
|
"create": "Réception créée avec succès",
|
||||||
"create": "Impossible de créer la réception.",
|
"update": "Réception mise à jour avec succès."
|
||||||
"update": "Impossible de mettre à jour la réception.",
|
},
|
||||||
"weigh": "Impossible de récupérer la pesée."
|
"shipment": {
|
||||||
|
"create": "Éxpedition créée avec succès",
|
||||||
|
"update": "Éxpedition mise à jour 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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"reception": {
|
|
||||||
"update": "Réception mise à jour avec succès."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
@@ -1,43 +1,270 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-white text-neutral-900">
|
<div class="min-h-screen text-neutral-900 flex flex-col">
|
||||||
<header class="w-full border-b border-neutral-200 bg-primary-500">
|
<!-- HEADER -->
|
||||||
<div class="flex w-full items-center px-6 py-4">
|
<header class="w-full bg-primary-500 py-5 px-6">
|
||||||
<NuxtLink to="/" class="flex items-center gap-3">
|
<div class="flex w-full items-center justify-between">
|
||||||
<span
|
<!-- Burger (mobile) -->
|
||||||
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
<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
|
LOGO
|
||||||
</span>
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<nav class="mx-8 flex gap-8 text-2xl font-bold uppercase text-white">
|
|
||||||
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
|
<!-- 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
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
:class="isExactActive ? 'opacity-100' : 'opacity-50'"
|
:class="route.path === '/'
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
>
|
>
|
||||||
Accueil
|
Accueil
|
||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/reception" custom v-slot="{ href, navigate, isActive }">
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/user/list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
:class="isReceptionActive ? 'opacity-100' : 'opacity-50'"
|
:class="route.path.startsWith('/admin/user')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
>
|
>
|
||||||
Reception
|
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>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</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>
|
</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>
|
</header>
|
||||||
<main class="mx-auto w-full max-w-[1050px] px-6 pt-[90px] pb-0">
|
<main class="mx-auto w-full max-w-[1280px] mt-16">
|
||||||
<slot/>
|
<slot/>
|
||||||
</main>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const isReceptionActive = computed(() => route.path.startsWith('/reception'))
|
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>
|
</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,13 +2,17 @@ export default defineNuxtConfig({
|
|||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
app: {
|
||||||
|
baseURL: process.env.NUXT_PUBLIC_APP_BASE || '/'
|
||||||
|
},
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxtjs/tailwindcss',
|
'@nuxtjs/tailwindcss',
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
'nuxt-toast',
|
'nuxt-toast',
|
||||||
'@nuxtjs/i18n'
|
'@nuxtjs/i18n',
|
||||||
|
'@nuxt/icon'
|
||||||
],
|
],
|
||||||
css: ['~/assets/css/toast.css'],
|
css: ['~/assets/css/main.css', '~/assets/css/toast.css'],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
||||||
@@ -16,7 +20,7 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
toast: {
|
toast: {
|
||||||
settings: {
|
settings: {
|
||||||
timeout: 0,
|
timeout: 10000,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
progressBar: false
|
progressBar: false
|
||||||
}
|
}
|
||||||
|
|||||||
184
frontend/package-lock.json
generated
184
frontend/package-lock.json
generated
@@ -7,6 +7,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/i18n": "^10.2.1",
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"izitoast": "^1.4.0",
|
"izitoast": "^1.4.0",
|
||||||
@@ -35,6 +36,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@antfu/install-pkg": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"package-manager-detector": "^1.3.0",
|
||||||
|
"tinyexec": "^1.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -63,6 +77,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -1063,7 +1078,6 @@
|
|||||||
"version": "4.12.2",
|
"version": "4.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1086,6 @@
|
|||||||
"version": "0.21.1",
|
"version": "0.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/object-schema": "^2.1.7",
|
"@eslint/object-schema": "^2.1.7",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
@@ -1086,7 +1099,6 @@
|
|||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -1096,7 +1108,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
},
|
},
|
||||||
@@ -1108,7 +1119,6 @@
|
|||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.17.0"
|
"@eslint/core": "^0.17.0"
|
||||||
},
|
},
|
||||||
@@ -1120,7 +1130,6 @@
|
|||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.15"
|
"@types/json-schema": "^7.0.15"
|
||||||
},
|
},
|
||||||
@@ -1132,7 +1141,6 @@
|
|||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
@@ -1155,7 +1163,6 @@
|
|||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -1165,7 +1172,6 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
@@ -1174,7 +1180,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
},
|
},
|
||||||
@@ -1186,7 +1191,6 @@
|
|||||||
"version": "9.39.2",
|
"version": "9.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||||
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
@@ -1198,7 +1202,6 @@
|
|||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
@@ -1207,7 +1210,6 @@
|
|||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.17.0",
|
"@eslint/core": "^0.17.0",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
@@ -1220,7 +1222,6 @@
|
|||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.0"
|
"node": ">=18.18.0"
|
||||||
}
|
}
|
||||||
@@ -1229,7 +1230,6 @@
|
|||||||
"version": "0.16.7",
|
"version": "0.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanfs/core": "^0.19.1",
|
"@humanfs/core": "^0.19.1",
|
||||||
"@humanwhocodes/retry": "^0.4.0"
|
"@humanwhocodes/retry": "^0.4.0"
|
||||||
@@ -1242,7 +1242,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22"
|
"node": ">=12.22"
|
||||||
},
|
},
|
||||||
@@ -1255,7 +1254,6 @@
|
|||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18"
|
"node": ">=18.18"
|
||||||
},
|
},
|
||||||
@@ -1264,6 +1262,47 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify/collections": {
|
||||||
|
"version": "1.0.646",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.646.tgz",
|
||||||
|
"integrity": "sha512-zA5Gr1MJm1SI0TjOUl7wu4kvBWXQ6Uh8ALEtqQ5ucXyUxP2M8m2bk2hfVtGykSdMlDB+Xs2AHbJ9pQqayz9WGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@iconify/types": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@iconify/utils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@antfu/install-pkg": "^1.1.0",
|
||||||
|
"@iconify/types": "^2.0.0",
|
||||||
|
"mlly": "^1.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@iconify/vue": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/cyberalien"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@intlify/bundle-utils": {
|
"node_modules/@intlify/bundle-utils": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-11.0.3.tgz",
|
||||||
@@ -2284,6 +2323,28 @@
|
|||||||
"devtools-wizard": "cli.mjs"
|
"devtools-wizard": "cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nuxt/icon": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-GI840yYGuvHI0BGDQ63d6rAxGzG96jQcWrnaWIQKlyQo/7sx9PjXkSHckXUXyX1MCr9zY6U25Td6OatfY6Hklw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/collections": "^1.0.641",
|
||||||
|
"@iconify/types": "^2.0.0",
|
||||||
|
"@iconify/utils": "^3.1.0",
|
||||||
|
"@iconify/vue": "^5.0.0",
|
||||||
|
"@nuxt/devtools-kit": "^3.1.1",
|
||||||
|
"@nuxt/kit": "^4.2.2",
|
||||||
|
"consola": "^3.4.2",
|
||||||
|
"local-pkg": "^1.1.2",
|
||||||
|
"mlly": "^1.8.0",
|
||||||
|
"ohash": "^2.0.11",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^3.10.0",
|
||||||
|
"tinyglobby": "^0.2.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nuxt/kit": {
|
"node_modules/@nuxt/kit": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.2.2.tgz",
|
||||||
@@ -2967,6 +3028,7 @@
|
|||||||
"version": "0.95.0",
|
"version": "0.95.0",
|
||||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.95.0.tgz",
|
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.95.0.tgz",
|
||||||
"integrity": "sha512-Te8fE/SmiiKWIrwBwxz5Dod87uYvsbcZ9JAL5ylPg1DevyKgTkxCXnPEaewk1Su2qpfNmry5RHoN+NywWFCG+A==",
|
"integrity": "sha512-Te8fE/SmiiKWIrwBwxz5Dod87uYvsbcZ9JAL5ylPg1DevyKgTkxCXnPEaewk1Su2qpfNmry5RHoN+NywWFCG+A==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "^0.95.0"
|
"@oxc-project/types": "^0.95.0"
|
||||||
},
|
},
|
||||||
@@ -4871,8 +4933,7 @@
|
|||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/parse-path": {
|
"node_modules/@types/parse-path": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
@@ -5222,6 +5283,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
|
||||||
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
|
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.28.5",
|
"@babel/parser": "^7.28.5",
|
||||||
"@vue/compiler-core": "3.5.26",
|
"@vue/compiler-core": "3.5.26",
|
||||||
@@ -5419,6 +5481,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5456,7 +5519,6 @@
|
|||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -5829,6 +5891,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -5942,6 +6005,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -6018,7 +6082,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -6137,6 +6200,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
}
|
}
|
||||||
@@ -6747,8 +6811,7 @@
|
|||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
@@ -7259,7 +7322,6 @@
|
|||||||
"version": "8.4.0",
|
"version": "8.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||||
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esrecurse": "^4.3.0",
|
"esrecurse": "^4.3.0",
|
||||||
"estraverse": "^5.2.0"
|
"estraverse": "^5.2.0"
|
||||||
@@ -7286,7 +7348,6 @@
|
|||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -7296,7 +7357,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -7308,7 +7368,6 @@
|
|||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
@@ -7320,7 +7379,6 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
},
|
},
|
||||||
@@ -7332,7 +7390,6 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
@@ -7341,7 +7398,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
},
|
},
|
||||||
@@ -7353,7 +7409,6 @@
|
|||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
"acorn-jsx": "^5.3.2",
|
"acorn-jsx": "^5.3.2",
|
||||||
@@ -7370,7 +7425,6 @@
|
|||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
@@ -7394,7 +7448,6 @@
|
|||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.1.0"
|
"estraverse": "^5.1.0"
|
||||||
},
|
},
|
||||||
@@ -7406,7 +7459,6 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.2.0"
|
"estraverse": "^5.2.0"
|
||||||
},
|
},
|
||||||
@@ -7504,8 +7556,7 @@
|
|||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-fifo": {
|
"node_modules/fast-fifo": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
@@ -7532,14 +7583,12 @@
|
|||||||
"node_modules/fast-json-stable-stringify": {
|
"node_modules/fast-json-stable-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-levenshtein": {
|
"node_modules/fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-npm-meta": {
|
"node_modules/fast-npm-meta": {
|
||||||
"version": "0.4.7",
|
"version": "0.4.7",
|
||||||
@@ -7580,7 +7629,6 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flat-cache": "^4.0.0"
|
"flat-cache": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -7610,7 +7658,6 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"locate-path": "^6.0.0",
|
"locate-path": "^6.0.0",
|
||||||
"path-exists": "^4.0.0"
|
"path-exists": "^4.0.0"
|
||||||
@@ -7626,7 +7673,6 @@
|
|||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||||
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
"keyv": "^4.5.4"
|
"keyv": "^4.5.4"
|
||||||
@@ -7638,8 +7684,7 @@
|
|||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
@@ -7906,7 +7951,6 @@
|
|||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -8207,7 +8251,6 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"parent-module": "^1.0.0",
|
"parent-module": "^1.0.0",
|
||||||
"resolve-from": "^4.0.0"
|
"resolve-from": "^4.0.0"
|
||||||
@@ -8223,7 +8266,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
@@ -8245,7 +8287,6 @@
|
|||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
@@ -8573,7 +8614,8 @@
|
|||||||
"node_modules/izitoast": {
|
"node_modules/izitoast": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/izitoast/-/izitoast-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/izitoast/-/izitoast-1.4.0.tgz",
|
||||||
"integrity": "sha512-Oc1X2wiQtPp39i5VpIjf3GJf5sfCtHKXZ5szx7RareyEeFLUlcEW0FSfBni28+Ul6KNKZRKzhVuWzSP4Xngh0w=="
|
"integrity": "sha512-Oc1X2wiQtPp39i5VpIjf3GJf5sfCtHKXZ5szx7RareyEeFLUlcEW0FSfBni28+Ul6KNKZRKzhVuWzSP4Xngh0w==",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
@@ -8631,20 +8673,17 @@
|
|||||||
"node_modules/json-buffer": {
|
"node_modules/json-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
@@ -8722,7 +8761,6 @@
|
|||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
@@ -8996,7 +9034,6 @@
|
|||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prelude-ls": "^1.2.1",
|
"prelude-ls": "^1.2.1",
|
||||||
"type-check": "~0.4.0"
|
"type-check": "~0.4.0"
|
||||||
@@ -9081,7 +9118,6 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-locate": "^5.0.0"
|
"p-locate": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -9119,8 +9155,7 @@
|
|||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.uniq": {
|
"node_modules/lodash.uniq": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@@ -9466,8 +9501,7 @@
|
|||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
@@ -10205,6 +10239,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.2.2.tgz",
|
||||||
"integrity": "sha512-n6oYFikgLEb70J4+K19jAzfx4exZcRSRX7yZn09P5qlf2Z59VNOBqNmaZO5ObzvyGUZ308SZfL629/Q2v2FVjw==",
|
"integrity": "sha512-n6oYFikgLEb70J4+K19jAzfx4exZcRSRX7yZn09P5qlf2Z59VNOBqNmaZO5ObzvyGUZ308SZfL629/Q2v2FVjw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dxup/nuxt": "^0.2.2",
|
"@dxup/nuxt": "^0.2.2",
|
||||||
"@nuxt/cli": "^3.31.1",
|
"@nuxt/cli": "^3.31.1",
|
||||||
@@ -10472,7 +10507,6 @@
|
|||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-is": "^0.1.3",
|
"deep-is": "^0.1.3",
|
||||||
"fast-levenshtein": "^2.0.6",
|
"fast-levenshtein": "^2.0.6",
|
||||||
@@ -10519,6 +10553,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.102.0.tgz",
|
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.102.0.tgz",
|
||||||
"integrity": "sha512-xMiyHgr2FZsphQ12ZCsXRvSYzmKXCm1ejmyG4GDZIiKOmhyt5iKtWq0klOfFsEQ6jcgbwrUdwcCVYzr1F+h5og==",
|
"integrity": "sha512-xMiyHgr2FZsphQ12ZCsXRvSYzmKXCm1ejmyG4GDZIiKOmhyt5iKtWq0klOfFsEQ6jcgbwrUdwcCVYzr1F+h5og==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "^0.102.0"
|
"@oxc-project/types": "^0.102.0"
|
||||||
},
|
},
|
||||||
@@ -10591,7 +10626,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yocto-queue": "^0.1.0"
|
"yocto-queue": "^0.1.0"
|
||||||
},
|
},
|
||||||
@@ -10606,7 +10640,6 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-limit": "^3.0.2"
|
"p-limit": "^3.0.2"
|
||||||
},
|
},
|
||||||
@@ -10633,7 +10666,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"callsites": "^3.0.0"
|
"callsites": "^3.0.0"
|
||||||
},
|
},
|
||||||
@@ -10682,7 +10714,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -10797,6 +10828,7 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-api": "^7.7.7"
|
"@vue/devtools-api": "^7.7.7"
|
||||||
},
|
},
|
||||||
@@ -10902,6 +10934,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -11451,6 +11484,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
@@ -11518,7 +11552,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
@@ -11573,7 +11606,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -11925,6 +11957,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||||
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
|
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -12484,7 +12517,6 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
},
|
},
|
||||||
@@ -12747,6 +12779,7 @@
|
|||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -13086,7 +13119,6 @@
|
|||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prelude-ls": "^1.2.1"
|
"prelude-ls": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -13156,6 +13188,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -13597,7 +13630,6 @@
|
|||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -13623,6 +13655,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -13979,6 +14012,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.26",
|
"@vue/compiler-dom": "3.5.26",
|
||||||
"@vue/compiler-sfc": "3.5.26",
|
"@vue/compiler-sfc": "3.5.26",
|
||||||
@@ -14014,6 +14048,7 @@
|
|||||||
"version": "11.2.8",
|
"version": "11.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
|
||||||
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
|
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.2.8",
|
"@intlify/core-base": "11.2.8",
|
||||||
"@intlify/shared": "11.2.8",
|
"@intlify/shared": "11.2.8",
|
||||||
@@ -14034,6 +14069,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-api": "^6.6.4"
|
"@vue/devtools-api": "^6.6.4"
|
||||||
},
|
},
|
||||||
@@ -14085,7 +14121,6 @@
|
|||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -14351,7 +14386,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"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",
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"izitoast": "^1.4.0",
|
"izitoast": "^1.4.0",
|
||||||
|
|||||||
110
frontend/pages/admin/bovin/[[id]].vue
Normal file
110
frontend/pages/admin/bovin/[[id]].vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<form @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" />
|
||||||
|
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" />
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
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 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>
|
||||||
113
frontend/pages/admin/carrier/[[id]].vue
Normal file
113
frontend/pages/admin/carrier/[[id]].vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<form @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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiTextInput
|
||||||
|
label="Code transporteur"
|
||||||
|
id="code-id"
|
||||||
|
v-model="form.code"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
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 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>
|
||||||
202
frontend/pages/admin/customer/[[id]].vue
Normal file
202
frontend/pages/admin/customer/[[id]].vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<form @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]"/>
|
||||||
|
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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">Libellé</th>
|
||||||
|
<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="6" 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.label || "—" }}</td>
|
||||||
|
<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 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,
|
||||||
|
label: address.label ?? "",
|
||||||
|
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 created = await createCustomer(customerPayload)
|
||||||
|
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>
|
||||||
117
frontend/pages/admin/customer/customer-list.vue
Normal file
117
frontend/pages/admin/customer/customer-list.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<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-8 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>Email</div>
|
||||||
|
<div>Rue</div>
|
||||||
|
<div>Complément</div>
|
||||||
|
<div>Code Postal</div>
|
||||||
|
<div>Ville</div>
|
||||||
|
<div>Pays</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">
|
||||||
|
<div
|
||||||
|
v-if="!customer.addresses || customer.addresses.length === 0"
|
||||||
|
class="grid text-primary-700 grid-cols-8 border-t gap-4 px-4 py-2 hover:bg-slate-50 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="col-span-1">Pas d'adresse</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="customer.addresses.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(address, idx) in customer.addresses"
|
||||||
|
:key="address.id ?? `${customer.id}-${idx}-${address.street}-${address.postalCode}`"
|
||||||
|
class="grid grid-cols-8 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
:class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''"
|
||||||
|
@click="goToCustomer(customer.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">
|
||||||
|
{{ idx === 0 ? (customer.name || "—") : "↳" }}
|
||||||
|
</div>
|
||||||
|
<div class="truncate">{{ idx === 0 ? (customer.phone || "—") : "" }}</div>
|
||||||
|
<div class="truncate">{{ idx === 0 ? (customer.email || "—") : "" }}</div>
|
||||||
|
<div class="truncate">{{ address.street || "—" }}</div>
|
||||||
|
<div class="truncate">{{ address.street2 || "—" }}</div>
|
||||||
|
<div>{{ address.postalCode || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.city || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.countryCode || "—" }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-8 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="col-span-5 text-slate-400">
|
||||||
|
Adresses non chargées
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
203
frontend/pages/admin/supplier/[[id]].vue
Normal file
203
frontend/pages/admin/supplier/[[id]].vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<form @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]"/>
|
||||||
|
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :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"
|
||||||
|
>
|
||||||
|
<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">Libellé</th>
|
||||||
|
<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="6" 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.label || "—" }}</td>
|
||||||
|
<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 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,
|
||||||
|
label: address.label ?? "",
|
||||||
|
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 created = await createSupplier(supplierPayload)
|
||||||
|
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>
|
||||||
113
frontend/pages/admin/supplier/supplier-list.vue
Normal file
113
frontend/pages/admin/supplier/supplier-list.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<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-7 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Mail</div>
|
||||||
|
<div>Rue</div>
|
||||||
|
<div>Complément</div>
|
||||||
|
<div>Code Postal</div>
|
||||||
|
<div>Ville</div>
|
||||||
|
<div>Pays</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">
|
||||||
|
<div
|
||||||
|
v-if="!supplier.addresses || supplier.addresses.length === 0"
|
||||||
|
class="grid text-primary-700 grid-cols-7 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ supplier.name }}</div>
|
||||||
|
<div class="truncate">{{ supplier.email }}</div>
|
||||||
|
<div class="col-span-1">Pas d'adresse</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="supplier.addresses.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(address, idx) in supplier.addresses"
|
||||||
|
:key="address.id ?? `${supplier.id}-${idx}-${address.street}-${address.postalCode}`"
|
||||||
|
class="grid grid-cols-7 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
:class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">
|
||||||
|
{{ idx === 0 ? supplier.name : "↳" }}
|
||||||
|
</div>
|
||||||
|
<div class="truncate">{{ idx === 0 ? supplier.email : "" }}</div>
|
||||||
|
<div class="truncate">{{ address.street || "—" }}</div>
|
||||||
|
<div class="truncate">{{ address.street2 || "—" }}</div>
|
||||||
|
<div>{{ address.postalCode || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.city || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.countryCode || "—" }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-7 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.email }}</div>
|
||||||
|
<div class="col-span-5 text-slate-400">
|
||||||
|
Adresses non chargées
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
142
frontend/pages/admin/user/[[id]].vue
Normal file
142
frontend/pages/admin/user/[[id]].vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<form @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]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="user-role"
|
||||||
|
v-model="form.role"
|
||||||
|
label="Role de l'utilisateur"
|
||||||
|
:options="ROLE"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiTextInput
|
||||||
|
id="user-password"
|
||||||
|
v-model="form.password"
|
||||||
|
label="Mot de passe"
|
||||||
|
type="password"
|
||||||
|
: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 || isHydrating || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
<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 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,21 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="flex flex-wrap justify-center pb-16 gap-12">
|
||||||
<h1 class="text-3xl font-bold">Liste des receptions</h1>
|
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
||||||
<ul>
|
<card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" />
|
||||||
<li v-for="reception in receptionList" :key="reception.id">
|
<card-link label="PLAN DE SITE" link="/infrastructure/building" iconName="material-symbols:warehouse-outline-rounded" />
|
||||||
<NuxtLink :to="`/reception/${reception.id}`">Réception numéro {{ reception.id}}</NuxtLink>
|
<card-link link="/reception/waiting-reception" iconName="mdi:truck-remove-outline">
|
||||||
</li>
|
<template #label>
|
||||||
</ul>
|
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">
|
|
||||||
import type {ReceptionData} from "~/services/dto/reception-data";
|
|
||||||
import {getReceptionList} from "~/services/reception";
|
|
||||||
|
|
||||||
const receptionList = ref<ReceptionData[]>()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
receptionList.value = await getReceptionList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
242
frontend/pages/infrastructure/building.vue
Normal file
242
frontend/pages/infrastructure/building.vue
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<!-- En-tête de page avec retour et titre -->
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<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">bâtiments</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px] space-y-6">
|
||||||
|
<!-- Liste des bâtiments + rendu du plan de chaque bâtiment -->
|
||||||
|
<div
|
||||||
|
v-for="entry in buildingLayouts"
|
||||||
|
:key="entry.building.id"
|
||||||
|
>
|
||||||
|
<div class="font-semibold tracking-wide text-primary-500">
|
||||||
|
{{ entry.building.label || `Bâtiment ${entry.building.id}` }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-4">
|
||||||
|
<!-- Aucun layout disponible pour ce bâtiment -->
|
||||||
|
<div v-if="!entry.layout" class="text-sm text-slate-400">
|
||||||
|
Aucun plan de bâtiment.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grille CSS : les cases sont positionnées via spanStyle -->
|
||||||
|
<div v-else class="overflow-auto">
|
||||||
|
<div class="grid" :style="entry.gridStyle">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="cell in entry.cells"
|
||||||
|
:key="cell.key"
|
||||||
|
class="relative text-white flex h-[50px] items-center justify-center border-y-[3px] border-y-black bg-white hover:opacity-85 focus-visible:outline-none"
|
||||||
|
:class="[cell.sideBorderClass, activeLegendStatutId !== null && cell.caseStatusId !== activeLegendStatutId ? 'opacity-35 hover:opacity-70' : '']"
|
||||||
|
:style="[cell.spanStyle, cell.sideBorderStyle]"
|
||||||
|
:to="cell.caseId ? `/infrastructure/case?id=${cell.caseId}` : '/infrastructure/case'"
|
||||||
|
:title="cell.caseStatusLabel ?? undefined"
|
||||||
|
>
|
||||||
|
<!-- Le blanc latéral est géré sur ce bloc interne (conditionnel par voisinage) -->
|
||||||
|
<div
|
||||||
|
class="flex h-full w-full items-center justify-center bg-white"
|
||||||
|
:class="cell.contentInsetClass"
|
||||||
|
:style="cell.caseStyle"
|
||||||
|
>
|
||||||
|
<!-- Numéro de case -->
|
||||||
|
{{ cell.display }}
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Légende : survol d'un statut => atténue les autres cases -->
|
||||||
|
<div class="py-4">
|
||||||
|
<!-- 3 zones fixes pour forcer gauche / centre / droite sur toute la largeur -->
|
||||||
|
<div class="grid w-full grid-cols-3 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="(statut, index) in statutLegend"
|
||||||
|
:key="statut.id"
|
||||||
|
class="flex min-w-0 cursor-pointer items-center gap-2 py-1"
|
||||||
|
:class="[
|
||||||
|
index === 0 ? 'justify-self-start' : '',
|
||||||
|
index === statutLegend.length - 1 ? 'justify-self-end' : '',
|
||||||
|
index > 0 && index < statutLegend.length - 1 ? 'justify-self-center' : ''
|
||||||
|
]"
|
||||||
|
@mouseenter="activeLegendStatutId = statut.id"
|
||||||
|
@mouseleave="activeLegendStatutId = null"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="h-5 w-5 border border-slate-300"
|
||||||
|
:style="statut.couleur ? { backgroundColor: statut.couleur } : {}"
|
||||||
|
></span>
|
||||||
|
<span class="truncate text-sm uppercase text-slate-700">
|
||||||
|
{{ statut.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {BuildingData} from "~/services/dto/building-data"
|
||||||
|
import type {BuildingLayoutData} from "~/services/dto/building-layout-data"
|
||||||
|
import type {BuildingCasePositionData} from "~/services/dto/building-case-position-data"
|
||||||
|
import type {BuildingCaseStatusData} from "~/services/dto/building-case-status-data"
|
||||||
|
import {getBuildingList} from "~/services/building"
|
||||||
|
import {getStatutList} from "~/services/statut"
|
||||||
|
|
||||||
|
definePageMeta({layout: "default"})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
// Données brutes chargées depuis l'API
|
||||||
|
const buildingList = ref<BuildingData[]>([])
|
||||||
|
const statutLegend = ref<BuildingCaseStatusData[]>([])
|
||||||
|
// Statut actuellement survolé dans la légende (pour filtrage visuel)
|
||||||
|
const activeLegendStatutId = ref<number | null>(null)
|
||||||
|
// Modèle de vue prêt pour le template (layout + cellules + styles de grille)
|
||||||
|
const buildingLayouts = computed(() =>
|
||||||
|
buildingList.value.map((building) => {
|
||||||
|
// On affiche uniquement le premier layout du bâtiment
|
||||||
|
const layout = building.layouts?.[0] ?? null
|
||||||
|
const view = layout ? buildLayoutView(layout) : null
|
||||||
|
return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type GridCell = {
|
||||||
|
key: string
|
||||||
|
caseId: number | null
|
||||||
|
display: string
|
||||||
|
caseStatusId: number | null
|
||||||
|
caseStatusLabel: string | null
|
||||||
|
// Couleur de fond de la case (dépend du statut)
|
||||||
|
caseStyle?: Record<string, string>
|
||||||
|
// Placement dans la grille CSS (colonne/ligne de départ + span)
|
||||||
|
spanStyle: Record<string, string>
|
||||||
|
// Bordures latérales pointillées si la case touche un gap ou le bord du plan
|
||||||
|
sideBorderClass: string
|
||||||
|
// Couleur des bordures pointillées latérales (reprend la couleur de la cellule)
|
||||||
|
sideBorderStyle?: Record<string, string>
|
||||||
|
// Espace blanc interne uniquement côté(s) adjacent(s) à une autre case
|
||||||
|
contentInsetClass: string
|
||||||
|
}
|
||||||
|
// Type intermédiaire : garde des infos utiles au calcul des bordures, retirées ensuite
|
||||||
|
type GridCellDraft = Omit<GridCell, "sideBorderClass" | "sideBorderStyle" | "contentInsetClass"> & { x: number; columnSpan: number }
|
||||||
|
|
||||||
|
// Nettoie la couleur de statut pour éviter les chaînes vides / espaces
|
||||||
|
const normalizeCaseStatusColor = (value: string | null | undefined): string | null => {
|
||||||
|
const color = (value ?? "").trim()
|
||||||
|
return color.length > 0 ? color : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles de base communs à toutes les grilles de bâtiments
|
||||||
|
const BASE_GRID_STYLE = {gridAutoRows: "1fr", rowGap: "18px", columnGap: "0px", width: "100%"} as const
|
||||||
|
|
||||||
|
// Transforme un layout API en structure de rendu (cellules + style de grille)
|
||||||
|
const buildLayoutView = (layout: BuildingLayoutData): {
|
||||||
|
cells: GridCell[];
|
||||||
|
gridStyle: Record<string, string>
|
||||||
|
} | null => {
|
||||||
|
const rows = layout.rows ?? 0, cols = layout.columns ?? 0
|
||||||
|
if (rows <= 0 || cols <= 0) return null
|
||||||
|
|
||||||
|
// Liste des positions de cases (filtre de sécurité sur les valeurs nulles)
|
||||||
|
const positions = (layout.casePositions ?? []).filter(Boolean) as BuildingCasePositionData[]
|
||||||
|
// Colonnes occupées par au moins une case (sert à détecter les gaps)
|
||||||
|
const occupiedColumns = new Set<number>()
|
||||||
|
// Sécurité : si deux positions ont le même x/y, on garde la première
|
||||||
|
const seenCoordinates = new Set<string>()
|
||||||
|
const cellDrafts: GridCellDraft[] = []
|
||||||
|
|
||||||
|
// Tri visuel : de haut en bas, puis de gauche à droite
|
||||||
|
const positionsSorted = [...positions].sort(
|
||||||
|
(leftPosition, rightPosition) =>
|
||||||
|
(leftPosition.y ?? 1) - (rightPosition.y ?? 1) || (leftPosition.x ?? 1) - (rightPosition.x ?? 1)
|
||||||
|
)
|
||||||
|
for (const position of positionsSorted) {
|
||||||
|
const x = position.x ?? 1
|
||||||
|
const y = position.y ?? 1
|
||||||
|
const coordinateKey = `${x}-${y}`
|
||||||
|
if (seenCoordinates.has(coordinateKey)) continue
|
||||||
|
seenCoordinates.add(coordinateKey)
|
||||||
|
|
||||||
|
// w/h = nombre de colonnes / lignes occupées par la case dans la grille
|
||||||
|
const columnSpan = position.w ?? 1
|
||||||
|
const rowSpan = position.h ?? 1
|
||||||
|
|
||||||
|
// Une case peut couvrir plusieurs colonnes : on les marque toutes comme occupées
|
||||||
|
for (let column = x; column < x + columnSpan; column++) {
|
||||||
|
if (column <= cols) occupiedColumns.add(column)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Métadonnées utiles au rendu / navigation / légende
|
||||||
|
const caseId = (position.buildingCase?.id ?? null) as number | null
|
||||||
|
const caseNumber = (position.buildingCase?.caseNumber ?? null) as number | null
|
||||||
|
const caseStatusId = position.buildingCase?.statut?.id ?? null
|
||||||
|
const caseStatusLabel = position.buildingCase?.statut?.label ?? null
|
||||||
|
const statusColor = normalizeCaseStatusColor(position.buildingCase?.statut?.couleur)
|
||||||
|
|
||||||
|
cellDrafts.push({
|
||||||
|
key: `case-${layout.id}-${position.id}`,
|
||||||
|
x,
|
||||||
|
columnSpan,
|
||||||
|
caseId,
|
||||||
|
display: caseNumber !== null ? String(caseNumber) : "Case",
|
||||||
|
caseStatusId,
|
||||||
|
caseStatusLabel,
|
||||||
|
caseStyle: statusColor ? {backgroundColor: statusColor} : undefined,
|
||||||
|
// Exemple : "14 / span 1" => commence en colonne 14 et occupe 1 colonne
|
||||||
|
spanStyle: {gridColumn: `${x} / span ${columnSpan}`, gridRow: `${y} / span ${rowSpan}`}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colonnes vides = gaps visuels (plus étroites dans la grille)
|
||||||
|
const gapColumns = Array.from({length: cols}, (_, i) => i + 1).filter((x) => !occupiedColumns.has(x))
|
||||||
|
const gapSet = new Set(gapColumns)
|
||||||
|
|
||||||
|
// Ajoute les bordures latérales pointillées pour les cases au contact d'un gap ou d'un bord
|
||||||
|
const cells: GridCell[] = cellDrafts.map(({x, columnSpan, ...cell}) => {
|
||||||
|
const touchesLeftGapOrEdge = x === 1 || gapSet.has(x - 1)
|
||||||
|
const touchesRightGapOrEdge = x + columnSpan - 1 === cols || gapSet.has(x + columnSpan)
|
||||||
|
const sideBorderClass = [
|
||||||
|
touchesLeftGapOrEdge ? "border-l-[3px] [border-left-style:dashed]" : "",
|
||||||
|
touchesRightGapOrEdge ? "border-r-[3px] [border-right-style:dashed]" : ""
|
||||||
|
].filter(Boolean).join(" ")
|
||||||
|
// Les pointillés latéraux reprennent la couleur de la cellule (si un statut en fournit une)
|
||||||
|
const sideBorderStyle = {
|
||||||
|
...(cell.caseStyle?.backgroundColor && touchesLeftGapOrEdge ? {borderLeftColor: cell.caseStyle.backgroundColor} : {}),
|
||||||
|
...(cell.caseStyle?.backgroundColor && touchesRightGapOrEdge ? {borderRightColor: cell.caseStyle.backgroundColor} : {})
|
||||||
|
}
|
||||||
|
// Le "blanc" n'est ajouté qu'entre deux cellules adjacentes (pas sur bord/gap)
|
||||||
|
const contentInsetClass = [
|
||||||
|
!touchesLeftGapOrEdge ? "ml-[4px]" : "",
|
||||||
|
!touchesRightGapOrEdge ? "mr-[4px]" : ""
|
||||||
|
].filter(Boolean).join(" ")
|
||||||
|
return {...cell, sideBorderClass, sideBorderStyle, contentInsetClass}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Les colonnes de gap sont rendues en 24px, les autres occupent l'espace restant
|
||||||
|
const columnsTemplate = Array.from({length: cols}, (_, i) => (gapSet.has(i + 1) ? "24px" : "minmax(0, 1fr)")).join(" ")
|
||||||
|
return {cells, gridStyle: {gridTemplateColumns: columnsTemplate, ...BASE_GRID_STYLE}}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Chargement initial des bâtiments et de la légende des statuts
|
||||||
|
const buildings = await getBuildingList()
|
||||||
|
const statuts = await getStatutList()
|
||||||
|
buildingList.value = buildings
|
||||||
|
// Tri alphabétique FR pour une légende stable
|
||||||
|
statutLegend.value = [...statuts].sort((a, b) =>
|
||||||
|
(a.label ?? "").localeCompare(b.label ?? "", "fr", {sensitivity: "base"})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
27
frontend/pages/infrastructure/case.vue
Normal file
27
frontend/pages/infrastructure/case.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="!hasCaseId"
|
||||||
|
@click="printCaseReport"
|
||||||
|
>
|
||||||
|
Imprimer
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const caseId = computed(() => Number(route.query.id))
|
||||||
|
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
|
||||||
|
|
||||||
|
const printCaseReport = async () => {
|
||||||
|
if (!hasCaseId.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `tableau_poids_case_${caseId.value}.pdf`
|
||||||
|
await printPdf(`/building_cases/${caseId.value}/weights-report`, filename)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
98
frontend/pages/login.vue
Normal file
98
frontend/pages/login.vue
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto w-full max-w-lg">
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
||||||
|
>
|
||||||
|
LOGO
|
||||||
|
</span>
|
||||||
|
<form
|
||||||
|
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-semibold text-neutral-700" for="user-select">
|
||||||
|
Utilisateur
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="user-select"
|
||||||
|
v-model="selectedUsername"
|
||||||
|
autocomplete="username"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
:disabled="isLoadingUsers"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Choisir un utilisateur</option>
|
||||||
|
<option v-for="user in users" :key="user.username" :value="user.username">
|
||||||
|
{{ user.username }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-semibold text-neutral-700" for="password">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Connexion
|
||||||
|
</UiButton>
|
||||||
|
<p class="font-bold">v{{ version }}</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import { getUsers } from '~/services/auth'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const { version } = useAppVersion()
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'auth'
|
||||||
|
})
|
||||||
|
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const isLoadingUsers = ref(true)
|
||||||
|
const selectedUsername = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
|
||||||
|
const isSubmitting = computed(() => {
|
||||||
|
return auth.isLoading || !selectedUsername.value || !password.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await auth.login(selectedUsername.value, password.value)
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,29 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex justify-between h-[52px] mb-[80px]">
|
||||||
<div class="flex justify-between h-[52px] mb-[90px]">
|
<div class="flex flex-1 mr-16">
|
||||||
<p class="self-center">Indicateur d’étapes</p>
|
<UiStepper
|
||||||
<button
|
:labels="RECEPTION_STEP_LABELS"
|
||||||
type="button"
|
:current-step="storeReception?.currentStep ?? 0"
|
||||||
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
@select="handleStepSelect"
|
||||||
@click="saveAndHold"
|
/>
|
||||||
>Mettre en attente</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
<UiButton
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
type="button"
|
||||||
<ReceptionUnloading v-if="storeReception?.currentStep === 2"/>
|
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
@click="saveAndHold"
|
||||||
|
>Mettre en attente
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||||
|
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
||||||
|
<ReceptionProductReceived
|
||||||
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
|
||||||
|
<ReceptionBovineReceived
|
||||||
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
||||||
|
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
import {useReceptionStore} from '~/stores/reception'
|
||||||
import { storeToRefs } from 'pinia'
|
import {storeToRefs} from 'pinia'
|
||||||
|
import {RECEPTION_STEP_LABELS} from '~/constants/steps'
|
||||||
|
import {RECEPTION_TYPE_CODES} from "~/utils/constants";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const { current: storeReception } = storeToRefs(receptionStore)
|
const {current: storeReception} = storeToRefs(receptionStore)
|
||||||
|
|
||||||
const resolveReceptionId = (param: unknown) => {
|
const resolveReceptionId = (param: unknown) => {
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
@@ -44,7 +56,7 @@ watch(
|
|||||||
}
|
}
|
||||||
await receptionStore.loadReception(id)
|
await receptionStore.loadReception(id)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{immediate: true}
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveAndHold = async () => {
|
const saveAndHold = async () => {
|
||||||
@@ -60,4 +72,19 @@ const saveAndHold = async () => {
|
|||||||
})
|
})
|
||||||
await router.push('/')
|
await router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStepSelect = async (step: number) => {
|
||||||
|
if (!receptionStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === receptionStore.current.currentStep) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
|
currentStep: step
|
||||||
|
})
|
||||||
|
await receptionStore.loadReception(receptionStore.current.id)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
62
frontend/pages/reception/finish-reception.vue
Normal file
62
frontend/pages/reception/finish-reception.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-start 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">listes des réceptions finie</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px]">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
|
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Numéro</div>
|
||||||
|
<div>Date</div>
|
||||||
|
<div>Fournisseur</div>
|
||||||
|
<div>Adresse</div>
|
||||||
|
<div>Type réception</div>
|
||||||
|
<div>Poids</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="reception in receptionList"
|
||||||
|
:key="reception.id"
|
||||||
|
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToReception(reception.id)"
|
||||||
|
>
|
||||||
|
<div>{{ reception.identificationNumber}}</div>
|
||||||
|
<div>{{ reception.receptionDate}}</div>
|
||||||
|
<div>{{ reception.supplier?.name }}</div>
|
||||||
|
<div>{{ reception.address?.fullAddress }}</div>
|
||||||
|
<div>{{ reception.receptionType?.label }}</div>
|
||||||
|
<div>{{ formatWeighing(reception) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {ReceptionData} from "~/services/dto/reception-data";
|
||||||
|
import {getReceptionList} from "~/services/reception";
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
|
||||||
|
const receptionList = ref<ReceptionData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formatWeighing = (reception: ReceptionData) => {
|
||||||
|
const gross = reception.weights?.find((weight) => weight.type === 'gross')?.weight
|
||||||
|
const tare = reception.weights?.find((weight) => weight.type === 'tare')?.weight
|
||||||
|
|
||||||
|
if (gross == null || tare == null) {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${gross - tare} kg`
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToReception = (id: number) => {
|
||||||
|
router.push(`/reception/update/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
receptionList.value = await getReceptionList(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
999
frontend/pages/reception/update/[[id]].vue
Normal file
999
frontend/pages/reception/update/[[id]].vue
Normal file
@@ -0,0 +1,999 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-[60px]">
|
||||||
|
<div class="flex items-center justify-between gap-10 relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] justify-between">
|
||||||
|
<Icon @click="router.push('/reception/finish-reception')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Réception {{ form.identificationNumber }}</h1>
|
||||||
|
<Icon @click="printReceipt" name="mdi:printer-outline" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<!-- Nom de l'utilisateur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-user"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<!-- Date de réception -->
|
||||||
|
<UiDateInput
|
||||||
|
id="reception-date"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
v-model="form.receptionDate"
|
||||||
|
label="Date de réception"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- type de reception -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-supplier"
|
||||||
|
v-model="form.receptionTypeId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label="Type de Réception"
|
||||||
|
:options="receptionTypes.map((receptionType) => ({
|
||||||
|
value: String(receptionType.id),
|
||||||
|
label: receptionType.label
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingSuppliers"
|
||||||
|
wrapper-class="col-start-1 row-start-4"
|
||||||
|
/>
|
||||||
|
<!-- Fournisseur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-supplier"
|
||||||
|
v-model="form.supplierId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label="Fournisseur"
|
||||||
|
:options="suppliers.map((supplier) => ({
|
||||||
|
value: String(supplier.id),
|
||||||
|
label: supplier.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingSuppliers"
|
||||||
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
/>
|
||||||
|
<!-- Adresse fournisseur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
label="Adresse"
|
||||||
|
:options="supplierAddresses.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))"
|
||||||
|
:disabled="(isLoadingSuppliers || supplierAddresses.length === 0) && !auth.isAdmin"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
<!-- Camion -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-truck"
|
||||||
|
v-model="form.truckId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label="Camion"
|
||||||
|
:options="trucks.map((truck) => ({
|
||||||
|
value: String(truck.id),
|
||||||
|
label: truck.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingTrucks"
|
||||||
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
/>
|
||||||
|
<!-- Transporteur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-carrier"
|
||||||
|
v-model="form.carrierId"
|
||||||
|
label="Transporteur"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
: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"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<!-- Plaque d'immatriculation -->
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Immatriculation (LIOT) -->
|
||||||
|
<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) && !auth.isAdmin"
|
||||||
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="formIsLoading">
|
||||||
|
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer "
|
||||||
|
:class="activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weights'"
|
||||||
|
>
|
||||||
|
pesée à plein
|
||||||
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer "
|
||||||
|
:class="activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500 ' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weightsEmpty'"
|
||||||
|
>
|
||||||
|
pesée à vide
|
||||||
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer "
|
||||||
|
:class="activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'merchandise'"
|
||||||
|
>
|
||||||
|
{{ isMerchandise ? "Marchandise" : "Bovins" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mb-12 ">
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weights'"
|
||||||
|
v-model="grossWeight"
|
||||||
|
v-if="grossWeight"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weightsEmpty'"
|
||||||
|
v-model="tareWeight"
|
||||||
|
v-if="tareWeight"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<update-merchandise
|
||||||
|
v-if="activeTab === 'merchandise' && isMerchandise"
|
||||||
|
v-model="merchandiseForm"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<update-bovin
|
||||||
|
v-if="activeTab === 'merchandise' && !isMerchandise"
|
||||||
|
v-model="bovineEntries"
|
||||||
|
v-model:otherQuantity="bovineOtherQuantity"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex mb-16 items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2 justify-self-end"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePdfPrinter } from '#imports'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import UpdateBovin from '~/components/reception/update-bovin.vue'
|
||||||
|
import UpdateMerchandise from '~/components/reception/update-merchandise.vue'
|
||||||
|
import UpdateWeight from '~/components/commun/update-weight.vue'
|
||||||
|
import { getUsers } from '~/services/auth'
|
||||||
|
import { getCarrierList } from '~/services/carrier'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
|
||||||
|
import type {
|
||||||
|
MerchandiseEntryData,
|
||||||
|
ReceptionData,
|
||||||
|
ReceptionFormData,
|
||||||
|
} from '~/services/dto/reception-data'
|
||||||
|
import type { WeightEntryData } from '~/services/dto/weight-data'
|
||||||
|
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||||
|
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||||
|
import type { TruckData } from '~/services/dto/truck-data'
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
import { getDriverList } from '~/services/driver'
|
||||||
|
import {
|
||||||
|
createReceptionBovine,
|
||||||
|
deleteReceptionBovine,
|
||||||
|
getReceptionBovineList,
|
||||||
|
updateReceptionBovine
|
||||||
|
} from '~/services/reception-bovine'
|
||||||
|
import {
|
||||||
|
createReceptionPelletBuilding,
|
||||||
|
deleteReceptionPelletBuilding,
|
||||||
|
getReceptionPelletBuildingList
|
||||||
|
} from '~/services/reception-pellet-building'
|
||||||
|
import { getReception, updateReception } from '~/services/reception'
|
||||||
|
import { getReceptionTypeList } from '~/services/reception-type'
|
||||||
|
import { getSupplierList } from '~/services/supplier'
|
||||||
|
import { getTruckList } from '~/services/truck'
|
||||||
|
import { getVehicleList } from '~/services/vehicle'
|
||||||
|
import { createWeight, updateWeight } from '~/services/weight'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
|
import { RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const receptionStore = useReceptionStore()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const activeTab = ref<'weightsEmpty' | 'weights' | 'merchandise'>('weights')
|
||||||
|
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
||||||
|
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
||||||
|
const bovineEntries = ref<ReceptionBovineTypeData[]>([])
|
||||||
|
const bovineOtherQuantity = ref<number | null>(0)
|
||||||
|
const merchandiseForm = ref<MerchandiseEntryData>({
|
||||||
|
merchandiseTypeId: '',
|
||||||
|
merchandiseDetail: '',
|
||||||
|
selectedBuildingIds: [],
|
||||||
|
selectedPelletBuildingIds: {}
|
||||||
|
})
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingTypes = ref(false)
|
||||||
|
const suppliers = ref<SupplierData[]>([])
|
||||||
|
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||||
|
const isLoadingSuppliers = ref(false)
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const formIsLoading = ref(false)
|
||||||
|
const isMerchandise = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const vehicleSyncLock = ref(false)
|
||||||
|
|
||||||
|
const idReception = Number(route.params.id)
|
||||||
|
|
||||||
|
function runWithVehicleSyncLock(mutator: () => void) {
|
||||||
|
if (vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vehicleSyncLock.value = true
|
||||||
|
try {
|
||||||
|
mutator()
|
||||||
|
} finally {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
vehicleSyncLock.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<ReceptionFormData>({
|
||||||
|
identificationNumber: null,
|
||||||
|
licensePlate: '',
|
||||||
|
receptionDate: new Date().toISOString().slice(0, 10),
|
||||||
|
receptionTypeId: '',
|
||||||
|
userId: '',
|
||||||
|
supplierId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
const supplierAddresses = computed(() => {
|
||||||
|
const supplierId = Number(form.supplierId)
|
||||||
|
if (!Number.isFinite(supplierId)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? []
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => idReception,
|
||||||
|
async (id) => {
|
||||||
|
if (!Number.isFinite(id) || id <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const reception = await getReception(id)
|
||||||
|
hydrateFromReception(reception)
|
||||||
|
await loadBovineEntries(id)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.receptionTypeId,
|
||||||
|
() => {
|
||||||
|
const receptionType = receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||||
|
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function createEmptyWeightEntry(type: 'gross' | 'tare'): WeightEntryData {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
dsd: null,
|
||||||
|
weight: null,
|
||||||
|
weighedAt: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearReceptionBovines(receptionId: number) {
|
||||||
|
const receptionIri = `/api/receptions/${receptionId}`
|
||||||
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
|
for (const selection of existing) {
|
||||||
|
await deleteReceptionBovine(selection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBovineEntries(receptionId: number) {
|
||||||
|
const receptionIri = `/api/receptions/${receptionId}`
|
||||||
|
bovineEntries.value = await getReceptionBovineList(receptionIri)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncMerchandiseFlag() {
|
||||||
|
const receptionType =
|
||||||
|
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||||
|
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 relation = value as { id?: number | string }
|
||||||
|
if (typeof relation.id === 'number') {
|
||||||
|
return String(relation.id)
|
||||||
|
}
|
||||||
|
if (typeof relation.id === 'string') {
|
||||||
|
return relation.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearReceptionMerchandise(receptionId: number) {
|
||||||
|
const receptionIri = `/api/receptions/${receptionId}`
|
||||||
|
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||||
|
for (const selection of existing) {
|
||||||
|
await deleteReceptionPelletBuilding(selection.id)
|
||||||
|
}
|
||||||
|
await updateReception(receptionId, {
|
||||||
|
merchandiseType: null,
|
||||||
|
merchandiseDetail: null,
|
||||||
|
buildings: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateFromReception(reception: ReceptionData | null) {
|
||||||
|
if (!reception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.identificationNumber = reception?.identificationNumber ?? ''
|
||||||
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
|
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
|
||||||
|
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)
|
||||||
|
: ''
|
||||||
|
form.receptionTypeId = reception?.receptionType?.id
|
||||||
|
? String(reception.receptionType.id)
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const selectionMap: Record<string, string[]> = {}
|
||||||
|
for (const selection of reception?.pelletBuildings ?? []) {
|
||||||
|
const pelletTypeId = getRelationId(selection.pelletType)
|
||||||
|
const buildingId = getRelationId(selection.building)
|
||||||
|
if (!pelletTypeId || !buildingId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!selectionMap[pelletTypeId]) {
|
||||||
|
selectionMap[pelletTypeId] = []
|
||||||
|
}
|
||||||
|
selectionMap[pelletTypeId].push(buildingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
merchandiseForm.value = {
|
||||||
|
merchandiseTypeId: reception?.merchandiseType?.id ? String(reception.merchandiseType.id) : '',
|
||||||
|
merchandiseDetail: reception?.merchandiseDetail ?? '',
|
||||||
|
selectedBuildingIds: reception?.buildings?.map((building) => String(building.id)) ?? [],
|
||||||
|
selectedPelletBuildingIds: selectionMap
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedOther =
|
||||||
|
typeof reception?.bovineDetail === 'string' && reception.bovineDetail.trim() !== ''
|
||||||
|
? Number(reception.bovineDetail)
|
||||||
|
: 0
|
||||||
|
bovineOtherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
|
||||||
|
const gross = reception.weights?.find((weight) => weight.type === 'gross') ?? null
|
||||||
|
const tare = reception.weights?.find((weight) => weight.type === 'tare') ?? null
|
||||||
|
grossWeight.value = gross ? { ...gross } : createEmptyWeightEntry('gross')
|
||||||
|
tareWeight.value = tare ? { ...tare } : createEmptyWeightEntry('tare')
|
||||||
|
isHydrating.value = false
|
||||||
|
syncMerchandiseFlag()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSuppliers() {
|
||||||
|
isLoadingSuppliers.value = true
|
||||||
|
try {
|
||||||
|
suppliers.value = await getSupplierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingSuppliers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTypes() {
|
||||||
|
isLoadingTypes.value = true
|
||||||
|
try {
|
||||||
|
receptionTypes.value = await getReceptionTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTypes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTrucks() {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCarriers() {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDrivers() {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadVehicles() {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultUser() {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printReceipt() {
|
||||||
|
if (!import.meta.client || !Number.isFinite(idReception) || idReception <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierName =
|
||||||
|
suppliers.value.find((supplier) => String(supplier.id) === form.supplierId)?.name ??
|
||||||
|
'fournisseur'
|
||||||
|
const filename = `${form.identificationNumber || idReception}_${supplierName}_${form.licensePlate || 'immat'}.pdf`
|
||||||
|
await printPdf(`/receptions/${idReception}/receipt`, filename)
|
||||||
|
|
||||||
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveWeightEntry(entry: WeightEntryData) {
|
||||||
|
if (!idReception || entry.weight === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: entry.type,
|
||||||
|
dsd: entry.dsd ?? null,
|
||||||
|
weight: entry.weight,
|
||||||
|
weighedAt: entry.weighedAt ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.id) {
|
||||||
|
await updateWeight(entry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if id is missing in local state, reuse existing weight by type.
|
||||||
|
const reception = await getReception(idReception)
|
||||||
|
const existingEntry = reception?.weights?.find((weight) => weight.type === entry.type) ?? null
|
||||||
|
if (existingEntry?.id) {
|
||||||
|
await updateWeight(existingEntry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWeight({
|
||||||
|
reception: `/api/receptions/${idReception}`,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function saveBovineEntry(entry: ReceptionBovineTypeData) {
|
||||||
|
if (!idReception || !entry.bovineType || entry.quantity === null || entry.quantity <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
quantity: entry.quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.id) {
|
||||||
|
await updateReceptionBovine(entry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createReceptionBovine({
|
||||||
|
reception: `/api/receptions/${idReception}`,
|
||||||
|
bovineType: `/api/bovine_types/${entry.bovineType.id}`,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTotalBovines() {
|
||||||
|
const totalTypes = bovineEntries.value.reduce(
|
||||||
|
(sum, entry) => sum + (entry.quantity ?? 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
return totalTypes +
|
||||||
|
(bovineOtherQuantity.value ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncBovineEntries() {
|
||||||
|
if (!idReception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const receptionIri = `/api/receptions/${idReception}`
|
||||||
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
|
const currentPositive = bovineEntries.value.filter(
|
||||||
|
(entry) => (entry.quantity ?? 0) > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const existingEntry of existing) {
|
||||||
|
const stillPresent = currentPositive.some(
|
||||||
|
(entry) => entry.bovineType.id === existingEntry.bovineType.id
|
||||||
|
)
|
||||||
|
if (!stillPresent) {
|
||||||
|
await deleteReceptionBovine(existingEntry.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of currentPositive) {
|
||||||
|
await saveBovineEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveMerchandiseEntry(
|
||||||
|
receptionIri: string,
|
||||||
|
pelletTypeId: string,
|
||||||
|
buildingId: string,
|
||||||
|
existingKeys: Set<string>
|
||||||
|
) {
|
||||||
|
const key = `${pelletTypeId}:${buildingId}`
|
||||||
|
if (existingKeys.has(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await createReceptionPelletBuilding({
|
||||||
|
reception: receptionIri,
|
||||||
|
pelletType: `/api/pellet_types/${pelletTypeId}`,
|
||||||
|
building: `/api/buildings/${buildingId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncMerchandiseEntries() {
|
||||||
|
if (!idReception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const receptionIri = `/api/receptions/${idReception}`
|
||||||
|
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||||
|
const existingMap = new Map<string, number>()
|
||||||
|
for (const selection of existing) {
|
||||||
|
const pelletTypeId = getRelationId(selection.pelletType)
|
||||||
|
const buildingId = getRelationId(selection.building)
|
||||||
|
if (!pelletTypeId || !buildingId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingMap.set(`${pelletTypeId}:${buildingId}`, selection.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
|
||||||
|
for (const [pelletTypeId, buildingIds] of Object.entries(
|
||||||
|
merchandiseForm.value.selectedPelletBuildingIds
|
||||||
|
)) {
|
||||||
|
for (const buildingId of buildingIds) {
|
||||||
|
desiredEntries.push({ pelletTypeId, buildingId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const desiredKeys = new Set(
|
||||||
|
desiredEntries.map((entry) => `${entry.pelletTypeId}:${entry.buildingId}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [key, selectionId] of existingMap.entries()) {
|
||||||
|
if (!desiredKeys.has(key)) {
|
||||||
|
await deleteReceptionPelletBuilding(selectionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingKeys = new Set(existingMap.keys())
|
||||||
|
for (const entry of desiredEntries) {
|
||||||
|
await saveMerchandiseEntry(
|
||||||
|
receptionIri,
|
||||||
|
entry.pelletTypeId,
|
||||||
|
entry.buildingId,
|
||||||
|
existingKeys
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 typeIri = normalizedReceptionTypeId
|
||||||
|
? `/api/reception_types/${normalizedReceptionTypeId}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
const basePayload = {
|
||||||
|
licensePlate: normalizedLicensePlate,
|
||||||
|
receptionDate: normalizedReceptionDate,
|
||||||
|
receptionType: typeIri,
|
||||||
|
user: userIri,
|
||||||
|
supplier: supplierIri,
|
||||||
|
address: addressIri,
|
||||||
|
truck: truckIri,
|
||||||
|
carrier: carrierIri
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...basePayload,
|
||||||
|
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idReception) {
|
||||||
|
await receptionStore.updateReception(idReception, {
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
await saveWeightEntry(grossWeight.value)
|
||||||
|
await saveWeightEntry(tareWeight.value)
|
||||||
|
|
||||||
|
if (isMerchandise.value) {
|
||||||
|
await clearReceptionBovines(idReception)
|
||||||
|
await updateReception(idReception, {
|
||||||
|
merchandiseType: merchandiseForm.value.merchandiseTypeId
|
||||||
|
? `/api/merchandise_types/${merchandiseForm.value.merchandiseTypeId}`
|
||||||
|
: null,
|
||||||
|
merchandiseDetail: merchandiseForm.value.merchandiseDetail.trim() || null,
|
||||||
|
buildings: merchandiseForm.value.selectedBuildingIds.map(
|
||||||
|
(buildingId) => `/api/buildings/${buildingId}`
|
||||||
|
),
|
||||||
|
bovineDetail: null,
|
||||||
|
bovinesTypes: null
|
||||||
|
})
|
||||||
|
await syncMerchandiseEntries()
|
||||||
|
} else {
|
||||||
|
if (getTotalBovines() > 52) {
|
||||||
|
// toast/erreur UI
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await clearReceptionMerchandise(idReception)
|
||||||
|
await syncBovineEntries()
|
||||||
|
await updateReception(idReception, {
|
||||||
|
bovineDetail: bovineOtherQuantity.value ? String(bovineOtherQuantity.value) : null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Évite une réhydratation complète après save (source de cascades de watchers).
|
||||||
|
// On recharge uniquement les bovins quand on est en mode bovins.
|
||||||
|
if (!isMerchandise.value) {
|
||||||
|
await loadBovineEntries(idReception)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadTypes()
|
||||||
|
syncMerchandiseFlag()
|
||||||
|
formIsLoading.value = true
|
||||||
|
await loadUsers()
|
||||||
|
await loadSuppliers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadDrivers()
|
||||||
|
await loadVehicles()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.supplierId, form.addressId, suppliers.value],
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.supplierId) {
|
||||||
|
if (form.addressId !== '') {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && supplierAddresses.value.length === 1) {
|
||||||
|
const nextAddressId = String(supplierAddresses.value[0].id)
|
||||||
|
if (form.addressId !== nextAddressId) {
|
||||||
|
form.addressId = nextAddressId
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = supplierAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (supplierAddresses.value.length === 1) {
|
||||||
|
const nextAddressId = String(supplierAddresses.value[0].id)
|
||||||
|
if (form.addressId !== nextAddressId) {
|
||||||
|
form.addressId = nextAddressId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (form.addressId !== '') {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value || vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.carrierId && idReception == null) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value && idReception == null) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredDrivers.value.length === 1) {
|
||||||
|
const nextDriverId = String(filteredDrivers.value[0].id)
|
||||||
|
if (form.driverId !== nextDriverId) {
|
||||||
|
form.driverId = nextDriverId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
const nextVehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
if (form.vehicleId !== nextVehicleId) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = nextVehicleId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.truckId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value || vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
const nextVehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
if (form.vehicleId !== nextVehicleId) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = nextVehicleId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = filteredVehicles.value.some(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (form.vehicleId !== '') {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.licensePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value || form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const match = filteredVehicles.value.find(
|
||||||
|
(vehicle) => vehicle.plate === form.licensePlate
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
const nextVehicleId = String(match.id)
|
||||||
|
if (form.vehicleId !== nextVehicleId) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = nextVehicleId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
</script>
|
||||||
51
frontend/pages/reception/waiting-reception.vue
Normal file
51
frontend/pages/reception/waiting-reception.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<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">listes des réceptions en attente</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px]">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16">
|
||||||
|
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Fournisseur</div>
|
||||||
|
<div>Adresse</div>
|
||||||
|
<div>Type réception</div>
|
||||||
|
<div>Transporteur</div>
|
||||||
|
<div>Immatriculation</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="reception in receptionList"
|
||||||
|
:key="reception.id"
|
||||||
|
class="grid grid-cols-5 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToReception(reception.id)"
|
||||||
|
@keydown.enter="goToReception(reception.id)"
|
||||||
|
>
|
||||||
|
<div>{{ reception.supplier?.name }}</div>
|
||||||
|
<div>{{ reception.address?.fullAddress }}</div>
|
||||||
|
<div>{{ reception.receptionType?.label }}</div>
|
||||||
|
<div>{{ reception.carrier?.name }}</div>
|
||||||
|
<div>{{ reception.licensePlate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {ReceptionData} from "~/services/dto/reception-data";
|
||||||
|
import {getReceptionList} from "~/services/reception";
|
||||||
|
|
||||||
|
const receptionList = ref<ReceptionData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goToReception = (id: number) => {
|
||||||
|
router.push(`/reception/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
receptionList.value = await getReceptionList(false)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
83
frontend/pages/shipment/[[id]].vue
Normal file
83
frontend/pages/shipment/[[id]].vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between h-[52px] mb-[80px]">
|
||||||
|
<div class="flex flex-1 mr-16">
|
||||||
|
<UiStepper
|
||||||
|
:labels="SHIPMENT_STEP_LABELS"
|
||||||
|
:current-step="storeShipment?.currentStep ?? 0"
|
||||||
|
@select="handleStepSelect"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UiButton
|
||||||
|
type="button"
|
||||||
|
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
||||||
|
@click="saveAndHold"
|
||||||
|
>Mettre en attente
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
||||||
|
<ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
|
||||||
|
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
||||||
|
<ShipmentWeight v-if="storeShipment?.currentStep === 3" mode="tare"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {SHIPMENT_STEP_LABELS} from "~/constants/steps";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useShipmentStore} from "~/stores/shipment";
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const {current: storeShipment} = storeToRefs(shipmentStore)
|
||||||
|
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const resolveShipmentId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch (
|
||||||
|
() => route.params.id,
|
||||||
|
async (param) => {
|
||||||
|
const id = resolveShipmentId(param)
|
||||||
|
if (id === null) {
|
||||||
|
shipmentStore.clearCurrent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await shipmentStore.loadShipment(id)
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveAndHold = async () => {
|
||||||
|
if (shipmentFormRef.value) {
|
||||||
|
await shipmentFormRef.value.saveDraft()
|
||||||
|
}
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
const handleStepSelect = async (step: number) => {
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === shipmentStore.current.currentStep) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: step
|
||||||
|
})
|
||||||
|
await shipmentStore.loadShipment(shipmentStore.current.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
85
frontend/pages/shipment/finish-shipment.vue
Normal file
85
frontend/pages/shipment/finish-shipment.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-start 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">listes des expéditions finie</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px]">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
|
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Numéro</div>
|
||||||
|
<div>Date</div>
|
||||||
|
<div>Client</div>
|
||||||
|
<div>Adresse</div>
|
||||||
|
<div>Type d'expéditon</div>
|
||||||
|
<div>Poids</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="shipment in shipmentList"
|
||||||
|
:key="shipment
|
||||||
|
.id"
|
||||||
|
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goShipment(shipment.id)"
|
||||||
|
>
|
||||||
|
<div>{{ shipment.identificationNumber }}</div>
|
||||||
|
<div>{{ shipment.shipmentDate }}</div>
|
||||||
|
<div>{{ shipment.customer?.name }}</div>
|
||||||
|
<div>{{ shipment.address?.fullAddress }}</div>
|
||||||
|
<div>
|
||||||
|
<template v-if="formatShipmentLines(shipment).length">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in formatShipmentLines(shipment)"
|
||||||
|
:key="index"
|
||||||
|
class="leading-5"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>{{ formatWeighing(shipment) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
import {getShipmentList} from "~/services/shipment";
|
||||||
|
|
||||||
|
const shipmentList = ref<ShipmentData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formatWeighing = (shipment: ShipmentData) => {
|
||||||
|
const gross = shipment.weights?.find((weight) => weight.type === 'gross')?.weight
|
||||||
|
const tare = shipment.weights?.find((weight) => weight.type === 'tare')?.weight
|
||||||
|
|
||||||
|
if (gross == null || tare == null) {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${gross - tare} kg`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const formatShipmentLines = (shipment: ShipmentData) => {
|
||||||
|
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = typeof shipment.shipmentType === 'string'
|
||||||
|
? shipment.shipmentType
|
||||||
|
: shipment.shipmentType?.label
|
||||||
|
|
||||||
|
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
const goShipment = (id: number) => {
|
||||||
|
router.push(`/shipment/update/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
shipmentList.value = await getShipmentList(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
639
frontend/pages/shipment/update/[[id]].vue
Normal file
639
frontend/pages/shipment/update/[[id]].vue
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
||||||
|
<div class="flex items-center justify-between gap-10 relative col-start-1 row-start-1">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] justify-between">
|
||||||
|
<Icon @click="router.push('/shipment/finish-shipment')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Expédition {{ form.identificationNumber }}</h1>
|
||||||
|
<Icon @click="printReceipt" name="mdi:printer-outline" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiDateInput
|
||||||
|
id="shipment-date"
|
||||||
|
v-model="form.shipmentDate"
|
||||||
|
label="Date d'expédition"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
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
|
||||||
|
}))"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
:options="customerAddressOptions"
|
||||||
|
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||||
|
label="Adresse"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="col-start-2 row-start-5 min-h-[72px]">
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="formIsLoading">
|
||||||
|
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
||||||
|
:class="activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weights'"
|
||||||
|
>
|
||||||
|
pesée à plein
|
||||||
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
|
||||||
|
:class="activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weightsEmpty'"
|
||||||
|
>
|
||||||
|
pesée à vide
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mb-12">
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weights'"
|
||||||
|
v-model="grossWeight"
|
||||||
|
v-if="grossWeight"
|
||||||
|
:isAdmin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weightsEmpty'"
|
||||||
|
v-model="tareWeight"
|
||||||
|
v-if="tareWeight"
|
||||||
|
:isAdmin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePdfPrinter } from '#imports'
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import UpdateWeight from '~/components/commun/update-weight.vue'
|
||||||
|
import { getUsers } from '~/services/auth'
|
||||||
|
import { getCarrierList } from '~/services/carrier'
|
||||||
|
import { getCustomerList } from '~/services/customer'
|
||||||
|
import type { AddressData } from '~/services/dto/address-data'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import type { CustomerData } from '~/services/dto/customer-data'
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { ShipmentData, ShipmentFormData } from '~/services/dto/shipment-data'
|
||||||
|
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
||||||
|
import type { TruckData } from '~/services/dto/truck-data'
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
import type { WeightEntryData } from '~/services/dto/weight-data'
|
||||||
|
import { getDriverList } from '~/services/driver'
|
||||||
|
import { getShipment, updateShipment } from '~/services/shipment'
|
||||||
|
import { getShipmentTypeList } from '~/services/shipment-type'
|
||||||
|
import { getTruckList } from '~/services/truck'
|
||||||
|
import { getVehicleList } from '~/services/vehicle'
|
||||||
|
import { createWeight, updateWeight } from '~/services/weight'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
import { SUPPLIER_CODE } from '~/utils/constants'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const customers = ref<CustomerData[]>([])
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
|
const currentShipment = ref<ShipmentData | null>(null)
|
||||||
|
|
||||||
|
const selectedShipmentTypeId = ref('')
|
||||||
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const activeTab = ref<'weightsEmpty' | 'weights'>('weights')
|
||||||
|
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
||||||
|
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
||||||
|
const formIsLoading = ref(false)
|
||||||
|
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingShipmentTypes = ref(false)
|
||||||
|
const isLoadingCustomers = ref(false)
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
|
||||||
|
const form = reactive<ShipmentFormData & { identificationNumber: string | null }>({
|
||||||
|
identificationNumber: null,
|
||||||
|
userId: '',
|
||||||
|
shipmentDate: new Date().toISOString().slice(0, 10),
|
||||||
|
customerId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: '',
|
||||||
|
licensePlate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const shipmentId = computed(() => {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
const isAddressData = (value: unknown): value is AddressData =>
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'id' in value &&
|
||||||
|
'fullAddress' in value
|
||||||
|
|
||||||
|
const customerAddresses = computed<AddressData[]>(() => {
|
||||||
|
if (!form.customerId) return []
|
||||||
|
const customerId = Number(form.customerId)
|
||||||
|
if (!Number.isFinite(customerId) || customerId <= 0) return []
|
||||||
|
|
||||||
|
const addresses = customers.value.find((c) => c.id === customerId)?.addresses ?? []
|
||||||
|
return addresses.filter(isAddressData)
|
||||||
|
})
|
||||||
|
|
||||||
|
const customerAddressOptions = computed(() =>
|
||||||
|
customerAddresses.value.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
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 loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadShipmentType = async () => {
|
||||||
|
isLoadingShipmentTypes.value = true
|
||||||
|
try {
|
||||||
|
bovineShipment.value = await getShipmentTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingShipmentTypes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCustomers = async () => {
|
||||||
|
isLoadingCustomers.value = true
|
||||||
|
try {
|
||||||
|
customers.value = await getCustomerList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCustomers.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 loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultUser() {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateFromShipment(shipment: ShipmentData | null) {
|
||||||
|
if (!shipment) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isHydrating.value = true
|
||||||
|
form.identificationNumber = shipment.identificationNumber ?? null
|
||||||
|
form.licensePlate = shipment.licensePlate ?? ''
|
||||||
|
form.shipmentDate = shipment.shipmentDate ?? 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
|
||||||
|
const gross = shipment.weights?.find((weight) => weight.type === 'gross') ?? null
|
||||||
|
const tare = shipment.weights?.find((weight) => weight.type === 'tare') ?? null
|
||||||
|
grossWeight.value = gross ? { ...gross } : createEmptyWeightEntry('gross')
|
||||||
|
tareWeight.value = tare ? { ...tare } : createEmptyWeightEntry('tare')
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printReceipt() {
|
||||||
|
if (!import.meta.client || shipmentId.value === null || shipmentId.value <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerName =
|
||||||
|
customers.value.find((customer) => String(customer.id) === form.customerId)?.name ??
|
||||||
|
'client'
|
||||||
|
const filename = `${form.identificationNumber || shipmentId.value}_${customerName}_${form.licensePlate || 'immat'}.pdf`
|
||||||
|
await printPdf(`/shipments/${shipmentId.value}/receipt`, filename)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadShipmentForUpdate() {
|
||||||
|
if (shipmentId.value === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const shipment = await getShipment(shipmentId.value)
|
||||||
|
currentShipment.value = shipment
|
||||||
|
hydrateFromShipment(shipment)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.customerId, form.addressId, customers.value],
|
||||||
|
() => {
|
||||||
|
if (!form.customerId) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = customerAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function applyLiotDefaults() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
applyLiotDefaults()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isHydrating.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) {
|
||||||
|
applyLiotDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value || isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selected = filteredVehicles.value.find(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
form.licensePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, form.vehicleId, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function 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 normalizedShipmentTypeId = selectedShipmentTypeId.value.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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyWeightEntry(type: 'gross' | 'tare'): WeightEntryData {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
dsd: null,
|
||||||
|
weight: null,
|
||||||
|
weighedAt: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveWeightEntry(entry: WeightEntryData) {
|
||||||
|
if (!shipmentId.value || entry.weight === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: entry.type,
|
||||||
|
dsd: entry.dsd ?? null,
|
||||||
|
weight: entry.weight,
|
||||||
|
weighedAt: entry.weighedAt ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.id) {
|
||||||
|
await updateWeight(entry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWeight({
|
||||||
|
shipment: `api/shipments/${shipmentId.value}`,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (shipmentId.value === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateShipment(shipmentId.value, {
|
||||||
|
currentStep: currentShipment.value?.currentStep ?? 0,
|
||||||
|
...buildPayload()
|
||||||
|
})
|
||||||
|
await saveWeightEntry(grossWeight.value)
|
||||||
|
await saveWeightEntry(tareWeight.value)
|
||||||
|
|
||||||
|
await loadShipmentForUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadShipmentType()
|
||||||
|
await loadUsers()
|
||||||
|
await loadCustomers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadVehicles()
|
||||||
|
await loadDrivers()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
formIsLoading.value = true
|
||||||
|
setDefaultUser()
|
||||||
|
await loadShipmentForUpdate()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user