Compare commits
2 Commits
v0.0.98
...
feat/202-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e1b431e57 | |||
| ce49785c79 |
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(npm run:*)",
|
|
||||||
"WebFetch(domain:geo.api.gouv.fr)",
|
|
||||||
"Bash(pip3 install:*)",
|
|
||||||
"Bash(python3 -c \":*)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
.env
50
.env
@@ -1,22 +1,48 @@
|
|||||||
APP_ENV=
|
# In all environments, the following files are loaded if they exist,
|
||||||
APP_DEBUG=
|
# the latter taking precedence over the former:
|
||||||
APP_SECRET=
|
#
|
||||||
|
# * .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
|
||||||
|
|
||||||
DEFAULT_URI=
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
APP_SHARE_DIR=var/share
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/routing ###
|
||||||
|
# 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=
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
###< nelmio/cors-bundle ###
|
###< nelmio/cors-bundle ###
|
||||||
|
|
||||||
###> lexik/jwt-authentication-bundle ###
|
###> lexik/jwt-authentication-bundle ###
|
||||||
JWT_SECRET_KEY=
|
JWT_SECRET_KEY=
|
||||||
JWT_PUBLIC_KEY=
|
JWT_PUBLIC_KEY=
|
||||||
JWT_PASSPHRASE=
|
JWT_PASSPHRASE=
|
||||||
COOKIE_SECURE=
|
COOKIE_SECURE=1
|
||||||
###< lexik/jwt-authentication-bundle ###
|
###< lexik/jwt-authentication-bundle ###
|
||||||
|
|
||||||
# ADAPTER avec la vraie BDD (pas de variables docker)
|
|
||||||
DATABASE_URL=
|
|
||||||
|
|
||||||
PONT_BASCULE_BYPASS=
|
|
||||||
PONT_BASCULE_URL=
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
name: Build Release Artefact
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v0.0.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: "8.4"
|
|
||||||
extensions: mbstring, intl, pdo_pgsql, xml, curl, zip, gd
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install backend deps (prod)
|
|
||||||
env:
|
|
||||||
APP_ENV: prod
|
|
||||||
APP_DEBUG: "0"
|
|
||||||
run: composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
|
|
||||||
|
|
||||||
- name: Build frontend (static)
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
npm ci
|
|
||||||
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ NUXT_PUBLIC_GEO_API_BASE=https://geo.api.gouv.fr npm run generate
|
|
||||||
test -f .output/public/index.html
|
|
||||||
|
|
||||||
- name: Build artefact
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
mkdir -p release
|
|
||||||
tar -czf "release/ferme-${GITHUB_REF_NAME}.tar.gz" \
|
|
||||||
bin \
|
|
||||||
config \
|
|
||||||
migrations \
|
|
||||||
public \
|
|
||||||
src \
|
|
||||||
templates \
|
|
||||||
vendor \
|
|
||||||
composer.json \
|
|
||||||
composer.lock \
|
|
||||||
symfony.lock \
|
|
||||||
frontend/.output
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: release/ferme-${{ github.ref_name }}.tar.gz
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
###> 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
|
||||||
|
|||||||
6
.idea/copilot.data.migration.agent.xml
generated
6
.idea/copilot.data.migration.agent.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?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
6
.idea/copilot.data.migration.ask.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?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
6
.idea/copilot.data.migration.ask2agent.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?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
6
.idea/copilot.data.migration.edit.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?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
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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/data_source_mapping.xml
generated
Normal file
6
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourcePerFileMappings">
|
||||||
|
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/db-forest-config.xml
generated
6
.idea/db-forest-config.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?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>
|
|
||||||
8
.idea/ferme.iml
generated
8
.idea/ferme.iml
generated
@@ -147,14 +147,6 @@
|
|||||||
<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/lcobucci/jwt" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
<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" />
|
<excludePattern pattern="reference.php" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|||||||
263
.idea/php.xml
generated
263
.idea/php.xml
generated
@@ -4,184 +4,158 @@
|
|||||||
<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/psr/log" />
|
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||||
<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/polyfill-mbstring" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
|
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
<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/theseer/tokenizer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-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/deprecations" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
<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/event-manager" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
<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/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/evenement/evenement" />
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
<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/object-reflector" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
<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/recursion-context" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
<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/webmozart/assert" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||||
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/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/serializer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
<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/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/phpdocumentor/type-resolver" />
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
<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/composer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/malio/ednotif-bundle" />
|
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
<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/validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||||
|
<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/options-resolver" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||||
|
<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/polyfill-php85" />
|
||||||
|
<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/polyfill-intl-grapheme" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||||
</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>
|
||||||
@@ -190,11 +164,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
805
.idea/workspace.xml
generated
805
.idea/workspace.xml
generated
@@ -4,12 +4,8 @@
|
|||||||
<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="fix : les non-admin ne peuvent plus supprimer de réception/expédition en attente">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de l'authentification avec lexik">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/waiting-reception.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/waiting-reception.vue" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/pages/shipment/waiting-shipment.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/shipment/waiting-shipment.vue" 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" />
|
||||||
@@ -20,11 +16,6 @@
|
|||||||
<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" />
|
||||||
@@ -33,7 +24,6 @@
|
|||||||
<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>
|
||||||
@@ -41,14 +31,11 @@
|
|||||||
<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="feature/FER-13-faire-des-recherches-sur-le-scanner-des-betes" />
|
<entry key="$PROJECT_DIR$" value="develop" />
|
||||||
</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 />
|
||||||
@@ -62,157 +49,149 @@
|
|||||||
</server>
|
</server>
|
||||||
</servers>
|
</servers>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="C:/php-8.4.3/php.exe">
|
<component name="PhpWorkspaceProjectConfiguration">
|
||||||
<include_path>
|
<include_path>
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||||
<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/polyfill-mbstring" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
|
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
<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/theseer/tokenizer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-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/deprecations" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
<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/event-manager" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
<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/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/evenement/evenement" />
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
<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/object-reflector" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
<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/recursion-context" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
<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/webmozart/assert" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||||
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/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/serializer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
||||||
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
||||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
<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/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/phpdocumentor/type-resolver" />
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
<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/composer" />
|
||||||
<path value="$PROJECT_DIR$/vendor/malio/ednotif-bundle" />
|
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
||||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
<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/validator" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||||
|
<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/options-resolver" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||||
|
<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/polyfill-php85" />
|
||||||
|
<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/polyfill-intl-grapheme" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||||
|
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectColorInfo">{
|
<component name="ProjectColorInfo">{
|
||||||
@@ -232,21 +211,16 @@
|
|||||||
"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": "fix/FER-15-fix-droit-de-suppression-reception-expedition-util",
|
"git-widget-placeholder": "feat/202-connexion-utilisateur",
|
||||||
"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": "advanced.settings",
|
"settings.editor.selected.configurable": "editor.preferences.tabs",
|
||||||
"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": [
|
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||||
"TEXT"
|
"TEXT"
|
||||||
],
|
],
|
||||||
@@ -256,29 +230,21 @@
|
|||||||
}
|
}
|
||||||
}]]></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="C:\Users\m-tristan\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
|
||||||
<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" />
|
||||||
</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.32098.40" />
|
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.29346.257" />
|
||||||
</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="" />
|
||||||
@@ -292,435 +258,88 @@
|
|||||||
<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="24805000" />
|
|
||||||
<workItem from="1774275549972" duration="51000" />
|
|
||||||
<workItem from="1774276665015" duration="33750000" />
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00037" summary="feat : finalisation de l'étape 1 "Réception" (formulaire)">
|
<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)">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769529522614</created>
|
<created>1768237763998</created>
|
||||||
<option name="number" value="00037" />
|
<option name="number" value="00001" />
|
||||||
<option name="presentableId" value="LOCAL-00037" />
|
<option name="presentableId" value="LOCAL-00001" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769529522614</updated>
|
<updated>1768237763998</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00038" summary="feat : ajout du numéro identification des receptions et ajustement du bon de reception">
|
<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">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769676223697</created>
|
<created>1768316052474</created>
|
||||||
<option name="number" value="00038" />
|
<option name="number" value="00002" />
|
||||||
<option name="presentableId" value="LOCAL-00038" />
|
<option name="presentableId" value="LOCAL-00002" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769676223697</updated>
|
<updated>1768316052474</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00039" summary="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception">
|
<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">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769700808988</created>
|
<created>1768316835575</created>
|
||||||
<option name="number" value="00039" />
|
<option name="number" value="00003" />
|
||||||
<option name="presentableId" value="LOCAL-00039" />
|
<option name="presentableId" value="LOCAL-00003" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769700808988</updated>
|
<updated>1768316835575</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00040" summary="feat : mise en place de composant UI pour les select, checkbox, date, text">
|
<task id="LOCAL-00004" summary="feat : update du fichier AGENTS.md">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769705141157</created>
|
<created>1768316965511</created>
|
||||||
<option name="number" value="00040" />
|
<option name="number" value="00004" />
|
||||||
<option name="presentableId" value="LOCAL-00040" />
|
<option name="presentableId" value="LOCAL-00004" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769705141157</updated>
|
<updated>1768316965511</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00041" summary="feat : update CHANGELOG.md">
|
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769705240487</created>
|
<created>1768317786187</created>
|
||||||
<option name="number" value="00041" />
|
<option name="number" value="00005" />
|
||||||
<option name="presentableId" value="LOCAL-00041" />
|
<option name="presentableId" value="LOCAL-00005" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769705240487</updated>
|
<updated>1768317786187</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00042" summary="feat : ajout de commentaire">
|
<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">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769760766200</created>
|
<created>1768318875533</created>
|
||||||
<option name="number" value="00042" />
|
<option name="number" value="00006" />
|
||||||
<option name="presentableId" value="LOCAL-00042" />
|
<option name="presentableId" value="LOCAL-00006" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769760766200</updated>
|
<updated>1768318875533</updated>
|
||||||
</task>
|
</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">
|
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769768517942</created>
|
<created>1768318921478</created>
|
||||||
<option name="number" value="00043" />
|
<option name="number" value="00007" />
|
||||||
<option name="presentableId" value="LOCAL-00043" />
|
<option name="presentableId" value="LOCAL-00007" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769768517943</updated>
|
<updated>1768318921478</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00044" summary="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception">
|
<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">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769770092190</created>
|
<created>1768498751836</created>
|
||||||
<option name="number" value="00044" />
|
<option name="number" value="00008" />
|
||||||
<option name="presentableId" value="LOCAL-00044" />
|
<option name="presentableId" value="LOCAL-00008" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769770092190</updated>
|
<updated>1768498751836</updated>
|
||||||
</task>
|
</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">
|
<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">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769770142624</created>
|
<created>1768555180530</created>
|
||||||
<option name="number" value="00045" />
|
<option name="number" value="00009" />
|
||||||
<option name="presentableId" value="LOCAL-00045" />
|
<option name="presentableId" value="LOCAL-00009" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769770142624</updated>
|
<updated>1768555180530</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00046" summary="feat : mise à jour du bon de réception">
|
<task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1769782099473</created>
|
<created>1768832208350</created>
|
||||||
<option name="number" value="00046" />
|
<option name="number" value="00010" />
|
||||||
<option name="presentableId" value="LOCAL-00046" />
|
<option name="presentableId" value="LOCAL-00010" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1769782099473</updated>
|
<updated>1768832208350</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00047" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
<option name="localTasksCounter" value="11" />
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770131226364</created>
|
|
||||||
<option name="number" value="00047" />
|
|
||||||
<option name="presentableId" value="LOCAL-00047" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770131226364</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00048" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770206668867</created>
|
|
||||||
<option name="number" value="00048" />
|
|
||||||
<option name="presentableId" value="LOCAL-00048" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770206668867</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00049" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770217875423</created>
|
|
||||||
<option name="number" value="00049" />
|
|
||||||
<option name="presentableId" value="LOCAL-00049" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770217875423</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00050" summary="feat : creer une nouvelle expedtion (WIP)">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770736570645</created>
|
|
||||||
<option name="number" value="00050" />
|
|
||||||
<option name="presentableId" value="LOCAL-00050" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770736570645</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00051" summary="feat : ajout d'une page de creation d'une expedition">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770880791564</created>
|
|
||||||
<option name="number" value="00051" />
|
|
||||||
<option name="presentableId" value="LOCAL-00051" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770880791565</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00052" summary="feat : changelog">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770881437439</created>
|
|
||||||
<option name="number" value="00052" />
|
|
||||||
<option name="presentableId" value="LOCAL-00052" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770881437439</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00053" summary="feat : lister les expeditions terminees">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770883114609</created>
|
|
||||||
<option name="number" value="00053" />
|
|
||||||
<option name="presentableId" value="LOCAL-00053" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770883114609</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00054" summary="feat : lister les expeditions terminees">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770884154297</created>
|
|
||||||
<option name="number" value="00054" />
|
|
||||||
<option name="presentableId" value="LOCAL-00054" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770884154297</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00055" summary="fix : corrections diverses">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770969471135</created>
|
|
||||||
<option name="number" value="00055" />
|
|
||||||
<option name="presentableId" value="LOCAL-00055" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770969471135</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00056" summary="fix : corrections frontend">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772094268366</created>
|
|
||||||
<option name="number" value="00056" />
|
|
||||||
<option name="presentableId" value="LOCAL-00056" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772094268366</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00057" summary="feat : affichage et modification expédition et modification bouton valider">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772111964268</created>
|
|
||||||
<option name="number" value="00057" />
|
|
||||||
<option name="presentableId" value="LOCAL-00057" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772111964268</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00058" summary="fix : erreur customer adress et bouton valider oublie">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772112729501</created>
|
|
||||||
<option name="number" value="00058" />
|
|
||||||
<option name="presentableId" value="LOCAL-00058" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772112729502</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00059" summary="feat : changelog update">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772112812677</created>
|
|
||||||
<option name="number" value="00059" />
|
|
||||||
<option name="presentableId" value="LOCAL-00059" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772112812677</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00060" summary="feat : changelog update">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772177400063</created>
|
|
||||||
<option name="number" value="00060" />
|
|
||||||
<option name="presentableId" value="LOCAL-00060" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772177400063</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00061" summary="feat : changelog update">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772177614438</created>
|
|
||||||
<option name="number" value="00061" />
|
|
||||||
<option name="presentableId" value="LOCAL-00061" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772177614438</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00062" summary="fix : color tab">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772178540489</created>
|
|
||||||
<option name="number" value="00062" />
|
|
||||||
<option name="presentableId" value="LOCAL-00062" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772178540489</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00063" summary="feat : modification front de la page admin transporteur">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772180053740</created>
|
|
||||||
<option name="number" value="00063" />
|
|
||||||
<option name="presentableId" value="LOCAL-00063" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772180053740</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00064" summary="fix : espacement et changelog">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772180581178</created>
|
|
||||||
<option name="number" value="00064" />
|
|
||||||
<option name="presentableId" value="LOCAL-00064" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772180581178</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00065" summary="fix : espacement">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772180684250</created>
|
|
||||||
<option name="number" value="00065" />
|
|
||||||
<option name="presentableId" value="LOCAL-00065" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772180684250</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00066" summary="fix : espacement">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772180972984</created>
|
|
||||||
<option name="number" value="00066" />
|
|
||||||
<option name="presentableId" value="LOCAL-00066" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772180972984</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00067" summary="fix : text">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772182545592</created>
|
|
||||||
<option name="number" value="00067" />
|
|
||||||
<option name="presentableId" value="LOCAL-00067" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772182545592</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00068" summary="feat : front page admin bovin et changelog">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772182707441</created>
|
|
||||||
<option name="number" value="00068" />
|
|
||||||
<option name="presentableId" value="LOCAL-00068" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772182707441</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00069" summary="fix : on ne bloque plus le poids max d'une pesée">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1772447581744</created>
|
|
||||||
<option name="number" value="00069" />
|
|
||||||
<option name="presentableId" value="LOCAL-00069" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1772447581744</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00070" summary="feat : ajout de supplier dans la feed et fixtures">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1773761787472</created>
|
|
||||||
<option name="number" value="00070" />
|
|
||||||
<option name="presentableId" value="LOCAL-00070" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1773761787472</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00071" summary="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1773766207721</created>
|
|
||||||
<option name="number" value="00071" />
|
|
||||||
<option name="presentableId" value="LOCAL-00071" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1773766207721</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00072" summary="fix : on ne pèse plus automatiquement + fix message de création réception">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1773826699115</created>
|
|
||||||
<option name="number" value="00072" />
|
|
||||||
<option name="presentableId" value="LOCAL-00072" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1773826699115</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00073" summary="fix : correction des retours de la V0">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1773841634554</created>
|
|
||||||
<option name="number" value="00073" />
|
|
||||||
<option name="presentableId" value="LOCAL-00073" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1773841634554</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00074" summary="feat : ajout de l'api de l'état pour chercher les villes via le CP">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1773842791819</created>
|
|
||||||
<option name="number" value="00074" />
|
|
||||||
<option name="presentableId" value="LOCAL-00074" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1773842791819</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00075" summary="fix : script de déploiement + CI/CD build de l'app">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1773843922376</created>
|
|
||||||
<option name="number" value="00075" />
|
|
||||||
<option name="presentableId" value="LOCAL-00075" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1773843922377</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00076" summary="fix : order navbar + modification création fournisseur et client">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1773852806120</created>
|
|
||||||
<option name="number" value="00076" />
|
|
||||||
<option name="presentableId" value="LOCAL-00076" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1773852806121</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00077" summary="fix : order récéption/expédition + correction style bouton récéption">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774283204849</created>
|
|
||||||
<option name="number" value="00077" />
|
|
||||||
<option name="presentableId" value="LOCAL-00077" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774283204849</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00078" summary="fix : style bon de récéption">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774285464091</created>
|
|
||||||
<option name="number" value="00078" />
|
|
||||||
<option name="presentableId" value="LOCAL-00078" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774285464091</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00079" summary="fix : bouton de mise en attente">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774337609427</created>
|
|
||||||
<option name="number" value="00079" />
|
|
||||||
<option name="presentableId" value="LOCAL-00079" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774337609427</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00080" summary="fix : problème de bearer token">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774448105945</created>
|
|
||||||
<option name="number" value="00080" />
|
|
||||||
<option name="presentableId" value="LOCAL-00080" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774448105945</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00081" summary="feat : système de blocage utilisateur">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774450388149</created>
|
|
||||||
<option name="number" value="00081" />
|
|
||||||
<option name="presentableId" value="LOCAL-00081" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774450388149</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00082" summary="feat : ajout d'un système de scanner bovin">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774543296474</created>
|
|
||||||
<option name="number" value="00082" />
|
|
||||||
<option name="presentableId" value="LOCAL-00082" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774543296474</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00083" summary="feat : mise à jour du CLAUDE.md">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774543626516</created>
|
|
||||||
<option name="number" value="00083" />
|
|
||||||
<option name="presentableId" value="LOCAL-00083" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774543626516</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00084" summary="feat : update CHANGELOG.md">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774543766582</created>
|
|
||||||
<option name="number" value="00084" />
|
|
||||||
<option name="presentableId" value="LOCAL-00084" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774543766582</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00085" summary="feat : la page de scanner est accessible que pour les admins">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1774543840891</created>
|
|
||||||
<option name="number" value="00085" />
|
|
||||||
<option name="presentableId" value="LOCAL-00085" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1774543840891</updated>
|
|
||||||
</task>
|
|
||||||
<option name="localTasksCounter" value="86" />
|
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -732,11 +351,6 @@
|
|||||||
<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" />
|
||||||
@@ -757,7 +371,7 @@
|
|||||||
<entry key="branch">
|
<entry key="branch">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
<option value="HEAD" />
|
<option value="develop" />
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
@@ -770,63 +384,22 @@
|
|||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="feat : changelog update" />
|
<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="fix : color tab" />
|
<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 : modification front de la page admin transporteur" />
|
<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 : espacement et changelog" />
|
<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="fix : espacement" />
|
<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="fix : text" />
|
<MESSAGE value="feat : update du fichier AGENTS.md" />
|
||||||
<MESSAGE value="feat : front page admin bovin et changelog" />
|
<MESSAGE value="feat : update du fichier README.md et CHANGELOG.md" />
|
||||||
<MESSAGE value="fix : on ne bloque plus le poids max d'une pesée" />
|
<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 de supplier dans la feed et fixtures" />
|
<MESSAGE value="test : ajout de TU sur les services et providers" />
|
||||||
<MESSAGE value="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu" />
|
<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="fix : on ne pèse plus automatiquement + fix message de création réception" />
|
<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 : correction des retours de la V0" />
|
<MESSAGE value="feat : ajout de l'authentification avec lexik" />
|
||||||
<MESSAGE value="feat : ajout de l'api de l'état pour chercher les villes via le CP" />
|
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout de l'authentification avec lexik" />
|
||||||
<MESSAGE value="fix : script de déploiement + CI/CD build de l'app" />
|
|
||||||
<MESSAGE value="fix : order navbar + modification création fournisseur et client" />
|
|
||||||
<MESSAGE value="fix : order récéption/expédition + correction style bouton récéption" />
|
|
||||||
<MESSAGE value="fix : style bon de récéption" />
|
|
||||||
<MESSAGE value="fix : bouton de mise en attente" />
|
|
||||||
<MESSAGE value="fix : problème de bearer token" />
|
|
||||||
<MESSAGE value="feat : système de blocage utilisateur" />
|
|
||||||
<MESSAGE value="feat : ajout d'un système de scanner bovin" />
|
|
||||||
<MESSAGE value="feat : mise à jour du CLAUDE.md" />
|
|
||||||
<MESSAGE value="feat : update CHANGELOG.md" />
|
|
||||||
<MESSAGE value="feat : la page de scanner est accessible que pour les admins" />
|
|
||||||
<MESSAGE value="fix : les non-admin ne peuvent plus supprimer de réception/expédition en attente" />
|
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="fix : les non-admin ne peuvent plus supprimer de réception/expédition en attente" />
|
|
||||||
</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>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
<select />
|
<select />
|
||||||
</component>
|
</component>
|
||||||
<component name="github-copilot-workspace">
|
|
||||||
<instructionFileLocations>
|
|
||||||
<option value=".github/instructions" />
|
|
||||||
</instructionFileLocations>
|
|
||||||
<promptFileLocations>
|
|
||||||
<option value=".github/prompts" />
|
|
||||||
</promptFileLocations>
|
|
||||||
</component>
|
|
||||||
</project>
|
</project>
|
||||||
46
AGENTS.md
Normal file
46
AGENTS.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 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
@@ -13,10 +13,6 @@ Ajouter dans le fichier .env
|
|||||||
- JWT_PUBLIC_KEY
|
- JWT_PUBLIC_KEY
|
||||||
- JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
- 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)
|
- 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
|
||||||
@@ -24,49 +20,7 @@ Ajouter dans le fichier .env du frontend
|
|||||||
### 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)
|
* [#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
|
|
||||||
* [#FER-11] Corriger le problème de bearer token
|
|
||||||
* [#FER-12] Ajouter un blocage des utilisateurs
|
|
||||||
* [#FER-13] Faire des recherches sur le scanner des bêtes
|
|
||||||
* [#FER-15] Les non-admin ne peuvent plus supprimer de réception/expédition en attente
|
|
||||||
* [#FER-17] Ecran d'ajout de bovin
|
|
||||||
* [#FER-18] Mise à jour du tableau d'arrivage
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
169
CLAUDE.md
169
CLAUDE.md
@@ -1,169 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
## Stack
|
|
||||||
|
|
||||||
- **Backend:** Symfony 8 + API Platform 4 (PHP 8.4)
|
|
||||||
- **Frontend:** Nuxt 4 (Vue 3, Pinia, Tailwind, Zod) in `frontend/`
|
|
||||||
- **Infra:** Docker (PHP-FPM + Nginx), Apache vhost serves API sous `/api` et frontend depuis `frontend/dist`
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker
|
|
||||||
make start # Démarrer les containers
|
|
||||||
make stop # Arrêter les containers
|
|
||||||
make restart # Redémarrer les containers
|
|
||||||
make shell # Shell dans le container PHP
|
|
||||||
|
|
||||||
# Install complet
|
|
||||||
make install # composer install + migrations + build frontend
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
make composer-install # Installer dépendances PHP
|
|
||||||
make migration-migrate # Lancer les migrations
|
|
||||||
make fixtures # Charger les fixtures
|
|
||||||
make cache-clear # Vider le cache Symfony
|
|
||||||
make test # Lancer les tests PHPUnit
|
|
||||||
make test FILES=tests/path/to/TestFile.php # Test spécifique
|
|
||||||
make php-cs-fixer-allow-risky FILES=src/... # Fixer le style
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
make build-nuxtJS # npm install + build:dist (dans le container)
|
|
||||||
make dev-nuxt # Serveur dev Nuxt (dans le container)
|
|
||||||
# Ou directement dans frontend/ :
|
|
||||||
cd frontend && npm run dev # Dev server (port 3000)
|
|
||||||
cd frontend && npm run build:dist # Build production
|
|
||||||
|
|
||||||
# Base de données
|
|
||||||
make db-reset # ⚠️ Supprime et recrée la BDD + migrations + fixtures
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture backend
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── ApiResource/ # Ressources API Platform custom
|
|
||||||
├── Command/ # Commandes Symfony (dont app:seed)
|
|
||||||
├── DataFixtures/ # Fixtures Doctrine
|
|
||||||
├── Dto/ # DTOs (ex: PontBasculeReading)
|
|
||||||
├── Entity/ # Entités Doctrine (= ressources API Platform)
|
|
||||||
├── Exception/ # Exceptions custom (PontBasculeException)
|
|
||||||
├── Kernel.php
|
|
||||||
├── Service/ # Services métier (PontBasculePayloadDecoder…)
|
|
||||||
└── State/ # State providers/processors API Platform
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture frontend
|
|
||||||
|
|
||||||
```
|
|
||||||
frontend/
|
|
||||||
├── components/
|
|
||||||
│ ├── ui/ # Composants réutilisables, auto-importés avec préfixe Ui (ex: UiLoadingDots)
|
|
||||||
│ ├── reception/ # Composants métier réception
|
|
||||||
│ ├── shipment/ # Composants métier expédition
|
|
||||||
│ ├── workflow/ # Composants partagés réception/expédition (workflow-weight, workflow-waiting-list, workflow-liot-fields)
|
|
||||||
│ └── commun/ # Composants communs (update-weight)
|
|
||||||
├── composables/ # useApi, useWeighing, usePdfPrinter, useAppVersion, useLiotHandling, useFormDataLoading, useAddressSync, useWorkflowSteps
|
|
||||||
│ └── steps/ # useWeighingStep (logique étape pesée)
|
|
||||||
├── config/ # reception.config.ts, shipment.config.ts (WorkflowConfig)
|
|
||||||
├── types/ # workflow.ts (interfaces partagées WorkflowEntity, WorkflowConfig, StepDefinition)
|
|
||||||
├── services/ # Couche service avec DTOs typés dans services/dto/
|
|
||||||
│ └── workflow-service.ts # Factory service API (createWorkflowService)
|
|
||||||
├── stores/ # Pinia stores (reception, shipment, auth)
|
|
||||||
│ └── workflow-store.ts # Factory store (useWorkflowStoreLogic)
|
|
||||||
├── pages/ # Pages Nuxt (file-based routing)
|
|
||||||
├── layouts/ # Layout default : max-width 1050px
|
|
||||||
├── i18n/locales/ # Traductions (défaut: fr)
|
|
||||||
├── utils/ # Constants, zod-errors helpers
|
|
||||||
└── assets/css/ # Tailwind config, main.css (font Helvetica)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conventions backend
|
|
||||||
|
|
||||||
- Code en anglais ; "pont-bascule" est un terme métier conservé tel quel.
|
|
||||||
- Les opérations API Platform sont définies directement sur les entités Doctrine.
|
|
||||||
- Repository custom autorisé dès qu'on a une requête métier non-triviale (agrégations, jointures spécifiques, filtres multiples). Toujours via DQL/QueryBuilder, **jamais de SQL brut** (pas de `Connection::executeQuery`, `fetchAssociative`, etc.). Les CRUD basiques restent sur le repo Doctrine par défaut via `EntityManagerInterface`.
|
|
||||||
- `config/reference.php` est auto-généré — ne pas modifier à la main.
|
|
||||||
- Endpoints toujours au pluriel (convention API Platform).
|
|
||||||
- Ne jamais créer de GET qui crée des ressources : utiliser POST + PATCH.
|
|
||||||
- Les noms de `Supplier`, `Customer` et `Carrier` sont automatiquement mis en majuscule via `mb_strtoupper` dans `setName()`.
|
|
||||||
|
|
||||||
## Conventions frontend
|
|
||||||
|
|
||||||
- SSR désactivé. Tailwind avec palette custom `primary` (ex: `bg-primary-500`).
|
|
||||||
- `useApi` (`composables/useApi.ts`) : méthodes `get/post/put/patch/delete` avec content-types par défaut.
|
|
||||||
- Toasts personnalisables via `toastErrorMessage`/`toastSuccessMessage` ou clés i18n `toastErrorKey`/`toastSuccessKey`.
|
|
||||||
- Utilise `useNuxtApp().$i18n` (pas `useI18n`) pour fonctionner hors setup.
|
|
||||||
- Validation formulaires avec Zod ; helpers dans `utils/zod-errors.ts`.
|
|
||||||
- Nav active : `NuxtLink` avec slot `custom`.
|
|
||||||
- PDFs : `usePdfPrinter` (receipt réception, rapport poids cases).
|
|
||||||
|
|
||||||
### Validation required & erreurs visuelles
|
|
||||||
- Les champs `required` utilisent l'attribut HTML natif forwardé via `v-bind="attrs"` dans les composants UI.
|
|
||||||
- La bordure rouge n'apparaît qu'après soumission grâce à la classe CSS `submitted` ajoutée sur le `<form>` au clic sur le bouton Valider (`@click="submitted = true"`).
|
|
||||||
- Règles CSS globales dans `main.css` : `.submitted :invalid` (bordure + texte rouge), `.submitted :has(:invalid) > label` et `.submitted label:has(:invalid)` (labels rouges).
|
|
||||||
- Pour les validations manuelles (checkboxes, radio groups), les messages d'erreur utilisent `invisible` (pas `hidden`) pour garder l'espace réservé et éviter les décalages de layout.
|
|
||||||
- Les dates de l'API sont renvoyées au format `Y-m-d H:i` ; les formulaires utilisent `.slice(0, 10)` pour extraire la date seule (compatible `<input type="date">`).
|
|
||||||
|
|
||||||
### Workflow réception/expédition (mutualisé)
|
|
||||||
- Factory service `createWorkflowService` et factory store `useWorkflowStoreLogic` pour éviter la duplication.
|
|
||||||
- Composables partagés : `useLiotHandling` (logique LIOT), `useFormDataLoading` (users, trucks, carriers), `useAddressSync` (sync adresse fournisseur/client).
|
|
||||||
- `useWeighing` : une seule fonction paramétrée pour réception et expédition (remplace `useWeighing` + `useWeighingShipment`).
|
|
||||||
- Configs workflow dans `config/reception.config.ts` et `config/shipment.config.ts` (étapes, labels pesée, filename PDF).
|
|
||||||
- `WorkflowWeight` composant partagé pour les étapes de pesée (remplace `reception-weight.vue` et `shipment-weight.vue`).
|
|
||||||
- `WorkflowWaitingList` composant partagé pour les listes en attente, avec support colonnes dynamiques, slot `actions`, et prop `showActions`.
|
|
||||||
|
|
||||||
## Domaine métier clé
|
|
||||||
|
|
||||||
### Réception (pesée pont-bascule)
|
|
||||||
- Entité principale `Reception` : `date_reception` (DateTimeImmutable, format lecture `Y-m-d H:i`, écriture `Y-m-d`), `identification_number` (auto `N-BR-####`), `current_step` (défaut 0), `is_valid` (défaut false).
|
|
||||||
- `Weight` (1-N avec Reception, cascade remove + orphanRemoval) : `type` (`gross`/`tare`), `dsd`, `weight`, `weighed_at`.
|
|
||||||
- Endpoint pesée : `/receptions/weigh` → `PontBasculeReading` (dsd, weight, weighedAt).
|
|
||||||
- Endpoint suppression : `DELETE /receptions/{id}` — supprime en cascade weights, pelletBuildings, bovines.
|
|
||||||
- Parsing payload pont-bascule : `Service/PontBasculePayloadDecoder.php`.
|
|
||||||
- Exception : `PontBasculeException` (messages en français, mappée 500).
|
|
||||||
- Store Pinia `reception.ts` = source de vérité pour la réception en cours.
|
|
||||||
- UI multi-étapes dans `pages/reception/[[id]].vue` basée sur `currentStep`.
|
|
||||||
- `PrePersist` : injecte l'heure courante sur `receptionDate` à la création ; `setReceptionDate` préserve l'heure existante au PATCH.
|
|
||||||
|
|
||||||
### Expédition
|
|
||||||
- Entité `Shipment` : même pattern que Reception, `shipment_date` (format lecture `Y-m-d H:i`, écriture `Y-m-d`).
|
|
||||||
- Endpoint suppression : `DELETE /shipments/{id}`.
|
|
||||||
- `PrePersist` : injecte l'heure courante sur `shipmentDate` ; `setShipmentDate` préserve l'heure au PATCH.
|
|
||||||
|
|
||||||
### LIOT (transport)
|
|
||||||
- Si carrier code = `LIOT` : afficher sélecteurs driver + vehicle, masquer saisie plaque manuelle.
|
|
||||||
- Liste véhicules filtrée par type de camion et transporteur.
|
|
||||||
- Le véhicule sélectionné alimente `license_plate`.
|
|
||||||
- Logique mutualisée dans `composables/useLiotHandling.ts`.
|
|
||||||
|
|
||||||
### Bovins & infrastructure
|
|
||||||
- `Bovine` : `nationalNumber` (unique), `receivedWeight`, `arrivalDate`, `buildingCase` (ManyToOne).
|
|
||||||
- `BuildingCase` a `bovines` (OneToMany).
|
|
||||||
- Rapport PDF cases : `GET /building_cases/{id}/weights-report` → template Twig, projection depuis `arrivalDate`, gain journalier fixe `1.3 kg/jour`.
|
|
||||||
|
|
||||||
### Scanner boucles auriculaires
|
|
||||||
- Page dédiée `/scan` : scan de codes-barres Code 39/128 (boucles auriculaires bovines) depuis un téléphone Android via Chrome.
|
|
||||||
- Utilise l'API native `BarcodeDetector` (Shape Detection API, Chrome Android 83+) — pas de lib JS, décodage hardware quasi-instantané.
|
|
||||||
- **Non supporté sur iOS** (tous les navigateurs iOS utilisent WebKit, qui n'implémente pas `BarcodeDetector`).
|
|
||||||
- Les 4 premiers caractères du code-barres sont retirés avant enregistrement (`rawValue.slice(4)`).
|
|
||||||
- Composable `useBarcodeScanner` : caméra arrière, anti-doublon 2s, vibration au scan.
|
|
||||||
- Le bovin est créé via `POST /bovines` avec `Content-Type: application/ld+json` (nécessaire pour la résolution d'IRI de `buildingCase`).
|
|
||||||
- Sélection bâtiment → case (filtrées dynamiquement) avant de scanner.
|
|
||||||
|
|
||||||
### Données de référence
|
|
||||||
- `ReceptionType`, `MerchandiseType`, `PelletType`, `Building`, `Supplier` (avec `Address` via join table, `createdBy` → User), `Customer` (avec `Address` via join table, `createdBy` → User), `Truck`, `Carrier`, `Driver`, `Vehicle`.
|
|
||||||
- `Address` : champ `label` nullable (déprécié, retiré du front et du `address:write`), expose `fullAddress` via getter. `countryCode` par défaut `FR` côté front.
|
|
||||||
|
|
||||||
### Seed & fixtures
|
|
||||||
- Commande `app:seed` : seed infrastructure (statut, building_layout, building_case, building_case_position) puis bovins.
|
|
||||||
- Utilise des flush intermédiaires pour que les queries find fonctionnent sur les records fraîchement créés.
|
|
||||||
- `upsertAddress` cherche par `street|postalCode` (plus par `label`).
|
|
||||||
- Fixtures : `BuildingInfrastructureFixtures` + `BovineFixtures` (via dépendances `AppFixtures`).
|
|
||||||
|
|
||||||
## Environnement
|
|
||||||
|
|
||||||
- API base dev : `http://localhost:8080/api` (via `NUXT_PUBLIC_API_BASE` dans `frontend/.env`)
|
|
||||||
- CORS : Nelmio, configurable via `CORS_ALLOW_ORIGIN` dans `.env`
|
|
||||||
- Locale par défaut : `fr` — traductions dans `frontend/i18n/locales/fr.json`
|
|
||||||
- Docker env : `docker/.env.docker` (défaut) avec override possible via `docker/.env.docker.local`
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# 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`
|
|
||||||
178
README.md
178
README.md
@@ -1,87 +1,68 @@
|
|||||||
# Projet Ferme t
|
# Projet Ferme
|
||||||
|
|
||||||
## Installation du projet
|
## Installation du projet
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
Pour windows, il faut installer le WSL2, Ubuntu, docker et nvm.
|
Pour windows, il faut installer le WSL2, Ubuntu, docker et nvm.
|
||||||
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows)
|
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows)
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
Pour linux, il faut installer docker et nvm.
|
Pour linux, il faut installer docker et nvm.
|
||||||
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux)
|
Il suffit de suivre cette [doc](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux)
|
||||||
|
|
||||||
### Installation du projet
|
### Installation du projet
|
||||||
|
|
||||||
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
Une fois les prérequis installés, il suffit de cloner le projet et de lancer les commandes suivantes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make start
|
make start
|
||||||
make install
|
make install
|
||||||
```
|
```
|
||||||
|
|
||||||
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
|
Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifier **POSTGRES_PORT** dans le fichier .env.docker.local, remplacer le par un port disponible.
|
||||||
|
|
||||||
### Configuration global
|
### Configuration global
|
||||||
|
|
||||||
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
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 (à 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)
|
||||||
- APP_SECRET (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));" et doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
* DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||||
- 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_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
* JWT_PUBLIC_KEY
|
||||||
- JWT_PUBLIC_KEY
|
* JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||||
- 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)
|
||||||
- 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="http://localhost:8080/api"
|
||||||
- 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>
|
||||||
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
||||||
|
* Name : ferme-docker
|
||||||
- Name : ferme-docker
|
* Host : localhost
|
||||||
- Host : localhost
|
* Port : 8080
|
||||||
- Port : 8080
|
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
||||||
- Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
|
||||||
|
|
||||||
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
|
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
|
||||||
|
|
||||||
## Utilisation du projet
|
## Utilisation du projet
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
L'api est disponible sur http://localhost:8080/api
|
L'api est disponible sur http://localhost:8080/api
|
||||||
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
|
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
|
||||||
Vous pouvez modifier le port si nécessaire.
|
Vous pouvez modifier le port si nécessaire.
|
||||||
|
|
||||||
La bdd est déja pré-configuré dans PhpStorm, il suffit de rentrer les infos du .env.docker.local pour se connecter.
|
La bdd est déja pré-configuré dans PhpStorm, il suffit de rentrer les infos du .env.docker.local pour se connecter.
|
||||||
C'est un bdd local dans le docker.
|
C'est un bdd local dans le docker.
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dev-nuxt
|
make dev-nuxt
|
||||||
```
|
```
|
||||||
|
|
||||||
Le front sera accessible sur http://localhost:3000
|
Le front sera accessible sur http://localhost:3000
|
||||||
|
|
||||||
### Authentification
|
### Authentification
|
||||||
|
|
||||||
Ce projet utilise l'authentification JWT avec un cookie httpOnly (LexikJWTAuthenticationBundle).
|
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.
|
Le frontend ne lit jamais directement le token, le navigateur envoie automatiquement le cookie.
|
||||||
|
|
||||||
### Login flow
|
### Login flow
|
||||||
|
|
||||||
- Frontend envoie les identifiants à:
|
- Frontend envoie les identifiants à:
|
||||||
- `POST /api/login_check`
|
- `POST /api/login_check`
|
||||||
- Backend returns:
|
- Backend returns:
|
||||||
@@ -90,165 +71,34 @@ Le frontend ne lit jamais directement le token, le navigateur envoie automatique
|
|||||||
- Le cookie est automatiquement envoyé pour les futures requêtes.
|
- Le cookie est automatiquement envoyé pour les futures requêtes.
|
||||||
- La déconnexion utilise `POST /api/logout` et redirige vers `/login`.
|
- 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
|
||||||
make restart
|
make restart
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour lancer les TU
|
Pour lancer les TU
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour accéder au container et lance des commandes
|
Pour accéder au container et lance des commandes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make shell
|
make shell
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour clear le cache Symfony
|
Pour clear le cache Symfony
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make cache-clear
|
make cache-clear
|
||||||
```
|
```
|
||||||
|
|
||||||
Faire une migration
|
Faire une migration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make migration-migrate
|
make migration-migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour générer un password pour un user
|
Pour générer un password pour un user
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make shell
|
make shell
|
||||||
php bin/console security:hash-password
|
php bin/console security:hash-password
|
||||||
```
|
```
|
||||||
|
|
||||||
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
INSERT INTO "user" (username, roles, password)
|
INSERT INTO "user" (username, roles, password)
|
||||||
VALUES ('Mon user', '["ROLE_USER"]', 'Mon mdp hashé');
|
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
|
|
||||||
|
|
||||||
## Feed des prix bovins
|
|
||||||
|
|
||||||
Une commande Symfony permet de mettre à jour le **poids à l'arrivée**, le **prix au kilo** et le **fournisseur** des bovins existants à partir d'un fichier XLSX. La commande **ne crée jamais de nouveau bovin** : elle complète seulement ceux déjà présents en BDD.
|
|
||||||
|
|
||||||
### Format du fichier XLSX attendu
|
|
||||||
|
|
||||||
Pas de ligne d'en-tête, 4 colonnes dans cet ordre :
|
|
||||||
|
|
||||||
| Colonne | Champ | Format | Exemple |
|
|
||||||
|---------|-------|--------|---------|
|
|
||||||
| A | Numéro national | Avec ou sans préfixe `FR ` (insensible casse) | `FR 7979580026` ou `7979580026` |
|
|
||||||
| B | Fournisseur | Texte libre, casse ignorée | `TERRENA` |
|
|
||||||
| C | Poids à l'arrivée (kg) | Entier | `368` |
|
|
||||||
| D | Prix au kilo | Décimal | `5.7` |
|
|
||||||
| E | Code bâtiment (optionnel) | `B1`, `B2`, `B3`, `ZT` (casse ignorée) | `B2` |
|
|
||||||
|
|
||||||
### Comportement
|
|
||||||
|
|
||||||
- **Numéro national** : le préfixe `FR` (avec ou sans espace) est retiré s'il est présent. Sinon la valeur est utilisée telle quelle.
|
|
||||||
- **Bovin introuvable** en BDD → ligne ignorée, log warning à la fin avec aperçu.
|
|
||||||
- **Fournisseur introuvable** en BDD → le bovin est mis à jour quand même avec `supplier = null`, log warning.
|
|
||||||
- **Bâtiment** (colonne E) : recherché par `code` (insensible casse). Set uniquement si le bovin n'a pas déjà une `buildingCase` assignée (la case prime sur le bâtiment direct côté affichage). Si code introuvable → log warning, champ non set.
|
|
||||||
- **Cellules `weight` / `price` vides ou non numériques** → champ non modifié.
|
|
||||||
- La commande est **idempotente** : peut être relancée sans effet de bord.
|
|
||||||
|
|
||||||
### Lancement en dev
|
|
||||||
|
|
||||||
Copie le fichier dans la racine du projet (mappée dans le container sous `/var/www/html`), puis :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Simulation sans écriture en BDD
|
|
||||||
docker compose exec php bin/console app:feed-bovine-prices /var/www/html/feed_bovin.xlsx --dry-run
|
|
||||||
|
|
||||||
# Persistance effective
|
|
||||||
docker compose exec php bin/console app:feed-bovine-prices /var/www/html/feed_bovin.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lancement en prod
|
|
||||||
|
|
||||||
Le user SSH n'a généralement pas les droits d'écriture sur `/var/www/ferme/` ; on passe donc le fichier par `/tmp` et on pointe la commande dessus (le chemin du XLSX est juste un argument).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Copier le fichier sur le serveur dans /tmp (accessible en écriture)
|
|
||||||
scp feed_bovin.xlsx <user>@<host>:/tmp/
|
|
||||||
|
|
||||||
# 2. SSH sur le serveur
|
|
||||||
ssh <user>@<host>
|
|
||||||
|
|
||||||
# 3. Se placer dans le dossier de l'app (pour bin/console)
|
|
||||||
cd /var/www/ferme
|
|
||||||
|
|
||||||
# 4. Dry-run pour vérifier sans rien écrire
|
|
||||||
php bin/console app:feed-bovine-prices /tmp/feed_bovin.xlsx --dry-run
|
|
||||||
|
|
||||||
# 5. Persistance effective
|
|
||||||
php bin/console app:feed-bovine-prices /tmp/feed_bovin.xlsx
|
|
||||||
|
|
||||||
# 6. Cleanup
|
|
||||||
rm /tmp/feed_bovin.xlsx
|
|
||||||
```
|
|
||||||
|
|
||||||
> Si à l'étape 4 le user PHP (souvent `www-data`) n'arrive pas à lire le fichier (`Permission denied`), donne-lui les droits de lecture avant : `chmod 644 /tmp/feed_bovin.xlsx`.
|
|
||||||
|
|
||||||
### Sortie attendue
|
|
||||||
|
|
||||||
À la fin, un tableau récapitule :
|
|
||||||
|
|
||||||
- Lignes totales lues
|
|
||||||
- Bovins mis à jour
|
|
||||||
- Bovins introuvables (avec aperçu des 10 premiers numéros)
|
|
||||||
- Lignes invalides (numéro national vide)
|
|
||||||
- Fournisseurs introuvables (avec liste et compte par nom)
|
|
||||||
- Bâtiments introuvables (avec liste des codes inconnus)
|
|
||||||
|
|||||||
@@ -14,10 +14,8 @@
|
|||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
"dompdf/dompdf": "^3.1",
|
"dompdf/dompdf": "^3.1",
|
||||||
"lexik/jwt-authentication-bundle": "*",
|
"lexik/jwt-authentication-bundle": "*",
|
||||||
"malio/ednotif-bundle": ">=0.0.6",
|
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpoffice/phpspreadsheet": "^5.7",
|
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
"symfony/asset": "8.0.*",
|
"symfony/asset": "8.0.*",
|
||||||
"symfony/console": "8.0.*",
|
"symfony/console": "8.0.*",
|
||||||
@@ -26,7 +24,6 @@
|
|||||||
"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.*",
|
||||||
@@ -89,19 +86,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
1761
composer.lock
generated
1761
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,17 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||||
use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle;
|
|
||||||
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||||
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||||
use Malio\EdnotifBundle\EdnotifBundle;
|
|
||||||
use Nelmio\CorsBundle\NelmioCorsBundle;
|
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||||
use Symfony\Bundle\MakerBundle\MakerBundle;
|
|
||||||
use Symfony\Bundle\MonologBundle\MonologBundle;
|
|
||||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||||
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
FrameworkBundle::class => ['all' => true],
|
FrameworkBundle::class => ['all' => true],
|
||||||
@@ -25,9 +20,4 @@ return [
|
|||||||
NelmioCorsBundle::class => ['all' => true],
|
NelmioCorsBundle::class => ['all' => true],
|
||||||
LexikJWTAuthenticationBundle::class => ['all' => true],
|
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
ApiPlatformBundle::class => ['all' => true],
|
ApiPlatformBundle::class => ['all' => true],
|
||||||
MonologBundle::class => ['all' => true],
|
|
||||||
EdnotifBundle::class => ['all' => true],
|
|
||||||
WebProfilerBundle::class => ['dev' => true],
|
|
||||||
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
|
||||||
MakerBundle::class => ['dev' => true],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ api_platform:
|
|||||||
stateless: true
|
stateless: true
|
||||||
cache_headers:
|
cache_headers:
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
pagination_client_items_per_page: true
|
|
||||||
pagination_maximum_items_per_page: 100
|
|
||||||
formats:
|
formats:
|
||||||
json: ['application/json']
|
json: ['application/json']
|
||||||
jsonld: ['application/ld+json']
|
jsonld: ['application/ld+json']
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
security:
|
security:
|
||||||
# Hiérarchie des rôles : ADMIN inclut BUREAU qui inclut USER.
|
|
||||||
# Ajouter un nouveau rôle = ajouter une ligne ici (et son équivalent côté
|
|
||||||
# front dans utils/roles.ts).
|
|
||||||
role_hierarchy:
|
|
||||||
ROLE_BUREAU: ROLE_USER
|
|
||||||
ROLE_ADMIN: ROLE_BUREAU
|
|
||||||
|
|
||||||
# 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'
|
App\Entity\User: 'auto'
|
||||||
@@ -27,7 +20,6 @@ security:
|
|||||||
pattern: ^/login_check
|
pattern: ^/login_check
|
||||||
stateless: true
|
stateless: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
user_checker: App\Security\UserChecker
|
|
||||||
json_login:
|
json_login:
|
||||||
check_path: /login_check
|
check_path: /login_check
|
||||||
username_path: username
|
username_path: username
|
||||||
@@ -38,10 +30,9 @@ security:
|
|||||||
pattern: ^/
|
pattern: ^/
|
||||||
stateless: true
|
stateless: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
user_checker: App\Security\UserChecker
|
|
||||||
jwt: ~
|
jwt: ~
|
||||||
logout:
|
logout:
|
||||||
path: /api/logout
|
path: /logout
|
||||||
target: /login
|
target: /login
|
||||||
enable_csrf: false
|
enable_csrf: false
|
||||||
delete_cookies:
|
delete_cookies:
|
||||||
@@ -56,15 +47,8 @@ security:
|
|||||||
|
|
||||||
# Note: Only the *first* matching rule is applied
|
# Note: Only the *first* matching rule is applied
|
||||||
access_control:
|
access_control:
|
||||||
# Login JWT
|
|
||||||
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
||||||
# Liste des users en lecture publique
|
- { path: ^/users, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||||
- { 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 }
|
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
when@dev:
|
|
||||||
web_profiler:
|
|
||||||
toolbar: true
|
|
||||||
|
|
||||||
framework:
|
|
||||||
profiler:
|
|
||||||
collect_serializer_data: true
|
|
||||||
1463
config/reference.php
1463
config/reference.php
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
resource: .
|
resource: .
|
||||||
type: api_platform
|
type: api_platform
|
||||||
prefix: /api
|
prefix: /
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
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,9 +8,6 @@
|
|||||||
# 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:
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
parameters:
|
|
||||||
app.version: '0.0.98'
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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:
|
||||||
php:
|
web:
|
||||||
container_name: php-${DOCKER_APP_NAME}-fpm
|
container_name: php-${DOCKER_APP_NAME}-apache
|
||||||
build:
|
build:
|
||||||
context: ./docker/php
|
context: ./docker/php
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -14,33 +14,23 @@ 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:
|
|
||||||
- "3000:3000"
|
|
||||||
restart: unless-stopped
|
|
||||||
nginx:
|
|
||||||
image: nginx:1.27-alpine
|
|
||||||
container_name: nginx-${DOCKER_APP_NAME}
|
|
||||||
depends_on:
|
|
||||||
- php
|
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
volumes:
|
- "3000:3000"
|
||||||
- ./:/var/www/html:ro
|
|
||||||
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|||||||
@@ -7,5 +7,3 @@ 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
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
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}-fpm-bullseye
|
FROM php:${DOCKER_PHP_VERSION}-apache-bullseye
|
||||||
|
|
||||||
ARG DOCKER_NODE_VERSION
|
ARG DOCKER_NODE_VERSION
|
||||||
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
||||||
@@ -102,14 +102,13 @@ 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
|
||||||
|
|||||||
32
docker/php/config/vhost.conf
Normal file
32
docker/php/config/vhost.conf
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<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>
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# Export Excel de l'inventaire bovin — Design Spec
|
|
||||||
|
|
||||||
Bouton sur la page `/inventory` qui télécharge un XLSX listant tous les bovins actuellement présents sur l'exploitation.
|
|
||||||
|
|
||||||
## Contexte
|
|
||||||
|
|
||||||
Le métier veut un Excel exportable depuis l'écran inventaire. Ferme n'a aujourd'hui aucun outil d'export Excel (uniquement PDF via dompdf). On choisit `phpoffice/phpspreadsheet` côté serveur, en suivant le même pattern que la génération PDF actuelle (endpoint qui streame le fichier, front qui télécharge via blob).
|
|
||||||
|
|
||||||
Périmètre : tous les bovins actifs (`exitedAt IS NULL`), ordre `birthDate ASC`, ignore les filtres UI. Pas de modale de sélection (à voir si le métier en demande une plus tard).
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
**Dépendance** : `composer require phpoffice/phpspreadsheet`
|
|
||||||
|
|
||||||
**Nouveau resource** : `src/ApiResource/BovineInventoryExport.php`
|
|
||||||
- `#[ApiResource]` avec une seule opération `Get` :
|
|
||||||
- `uriTemplate: '/bovines/inventory-export'`
|
|
||||||
- `output: false`
|
|
||||||
- `provider: BovineInventoryExportProvider::class`
|
|
||||||
- `security: "is_granted('ROLE_USER')"` (cohérent avec la page `/inventory`)
|
|
||||||
- OpenApi tag `Bovines`
|
|
||||||
|
|
||||||
**Nouveau provider** : `src/State/Bovin/BovineInventoryExportProvider.php`
|
|
||||||
- Injecte `EntityManagerInterface`
|
|
||||||
- Query Doctrine : `WHERE exitedAt IS NULL ORDER BY birthDate ASC`
|
|
||||||
- Construit le `Spreadsheet` avec PhpSpreadsheet
|
|
||||||
- Retourne une `Symfony\Component\HttpFoundation\Response` avec :
|
|
||||||
- `Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
|
||||||
- `Content-Disposition: attachment; filename="inventaire_bovins_YYYY-MM-DD.xlsx"`
|
|
||||||
- Body = `IOFactory::createWriter($spreadsheet, 'Xlsx')->save('php://output')` capturé via `ob_*`
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
**Page** : `frontend/pages/inventory.vue`
|
|
||||||
- Nouveau bouton "Exporter Excel" à droite du titre, à côté de "Rafraîchir"
|
|
||||||
- Style : même que "Rafraîchir" (bg-primary-500, h-[50px], icône `mdi:file-excel-outline`)
|
|
||||||
- Visible pour tout user authentifié (pas de gate admin)
|
|
||||||
- Au clic : appelle `useApi().getBlob('bovines/inventory-export')`, crée un blob URL, déclenche un `<a download>` synthétique avec le filename retourné par le backend (lu depuis le header `Content-Disposition`)
|
|
||||||
|
|
||||||
## Génération XLSX — détails
|
|
||||||
|
|
||||||
**Fichier** :
|
|
||||||
- 1 seule feuille `Inventaire`
|
|
||||||
- Filename : `inventaire_bovins_YYYY-MM-DD.xlsx` (date du jour serveur)
|
|
||||||
|
|
||||||
**En-têtes (ligne 1)** :
|
|
||||||
- 9 colonnes dans l'ordre : `N° National`, `N° Travail`, `Sexe`, `Né le`, `Age (mois)`, `Race`, `Bâtiment`, `Case`, `Entrée le`
|
|
||||||
- Style : gras, fond `#f1f5f9` (slate-100), bordure noire fine, alignement centré
|
|
||||||
- Auto-filter activé sur la plage des en-têtes (Excel ajoute les boutons de filtre natifs)
|
|
||||||
- Freeze pane : ligne 2 figée
|
|
||||||
|
|
||||||
**Lignes de données (à partir de la ligne 2)** :
|
|
||||||
- Ordre `birthDate ASC` (plus vieux en haut, NULL à la fin via `NULLS LAST` natif Postgres)
|
|
||||||
- Largeurs de colonnes :
|
|
||||||
- N° National : 18
|
|
||||||
- N° Travail : 12
|
|
||||||
- Sexe : 10
|
|
||||||
- Né le : 12
|
|
||||||
- Age : 12
|
|
||||||
- Race : 12
|
|
||||||
- Bâtiment : 30
|
|
||||||
- Case : 8
|
|
||||||
- Entrée le : 12
|
|
||||||
|
|
||||||
**Mapping des valeurs** :
|
|
||||||
- Sexe : `M` → `Mâle`, `F` → `Femelle`, autre / null → vide
|
|
||||||
- Né le, Entrée le : format `JJ/MM/AAAA`, vide si null
|
|
||||||
- Age : entier (mois), vide si null
|
|
||||||
- Bâtiment, Case : valeurs nestées via `bovine.buildingCase.building.label` et `bovine.buildingCase.caseNumber`, vide si null
|
|
||||||
|
|
||||||
**Couleurs des lignes** (basées sur `ageMonths`, mêmes seuils que l'UI) :
|
|
||||||
| Tranche | Hex | Tailwind |
|
|
||||||
|--------|-----|----------|
|
|
||||||
| 24+ mois | `#ddd6fe` | violet-200 |
|
|
||||||
| 22-24 mois | `#fecaca` | red-200 |
|
|
||||||
| 20-22 mois | `#fed7aa` | orange-200 |
|
|
||||||
| < 20 mois ou NULL | `#ffffff` | blanc |
|
|
||||||
|
|
||||||
Le fond est appliqué sur toute la ligne (9 cellules).
|
|
||||||
|
|
||||||
## Flux d'erreur
|
|
||||||
|
|
||||||
- Exception PhpSpreadsheet (création buffer) → propage en 500 standard API Platform
|
|
||||||
- Pas d'utilisateur (token expiré) → 401 standard via la sécurité
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
- 936 lignes × 9 colonnes : génération en mémoire < 1s, fichier < 100 KB
|
|
||||||
- Pas de pagination, pas de streaming row-by-row (overkill pour ce volume)
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Optionnel ce lot : test PHPUnit du provider qui vérifie que :
|
|
||||||
- Status 200
|
|
||||||
- Content-Type XLSX
|
|
||||||
- Header `Content-Disposition: attachment; filename=...xlsx`
|
|
||||||
- Body non vide
|
|
||||||
Mock simple de l'`EntityManagerInterface` pour retourner 2 bovins fictifs.
|
|
||||||
|
|
||||||
À faire en follow-up si on veut couvrir.
|
|
||||||
|
|
||||||
## Verification manuelle
|
|
||||||
|
|
||||||
1. `make composer-install` (après avoir ajouté la dep)
|
|
||||||
2. Recharger `/inventory`
|
|
||||||
3. Clic sur le bouton "Exporter Excel"
|
|
||||||
4. Vérifier le téléchargement : nom de fichier = `inventaire_bovins_2026-04-24.xlsx`
|
|
||||||
5. Ouvrir dans Excel/LibreOffice :
|
|
||||||
- 9 colonnes attendues
|
|
||||||
- En-tête figé en scrollant
|
|
||||||
- Auto-filter natif Excel
|
|
||||||
- Lignes colorées selon âge (violet/rouge/orange)
|
|
||||||
- Tri par date de naissance croissante
|
|
||||||
|
|
||||||
## Critères d'acceptation
|
|
||||||
|
|
||||||
- [ ] L'export contient 100 % des bovins actifs (count = `SELECT COUNT(*) FROM bovine WHERE exited_at IS NULL`)
|
|
||||||
- [ ] Le filename inclut la date du jour
|
|
||||||
- [ ] Les couleurs correspondent aux seuils d'âge
|
|
||||||
- [ ] L'ordre matche l'UI (`birthDate ASC`)
|
|
||||||
- [ ] Pas de régression sur les autres endpoints `/api/bovines`
|
|
||||||
@@ -3,11 +3,3 @@
|
|||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const { load } = useAppVersion()
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
load()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
body {
|
|
||||||
@apply font-sans;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.submitted :invalid {
|
|
||||||
@apply border-red-500 text-red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submitted :has(:invalid) > label {
|
|
||||||
@apply text-red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submitted label:has(:invalid) {
|
|
||||||
@apply text-red-500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form :class="{ submitted }" @submit.prevent="validateForm">
|
|
||||||
<div class="flex items-center mb-11 justify-between relative">
|
|
||||||
<div class="flex flex-row absolute -left-[60px] ">
|
|
||||||
<Icon @click="goBack" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
|
||||||
{{ props.address ? "Modification d'une adresse" : "Ajout d'une adresse" }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-y-16 gap-x-[200px] mb-16">
|
|
||||||
<UiTextInput id="address-street" v-model="form.street" label="Rue" required />
|
|
||||||
<UiTextInput id="address-street2" v-model="form.street2" label="Complément" />
|
|
||||||
<UiTextInput id="address-postalCode" v-model="form.postalCode" label="Code postal" required />
|
|
||||||
<UiSelect
|
|
||||||
id="address-city"
|
|
||||||
v-model="form.city"
|
|
||||||
label="Ville"
|
|
||||||
:options="communeOptions"
|
|
||||||
:loading="isLoadingCities"
|
|
||||||
:disabled="communes.length === 0"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiTextInput id="address-country" v-model="form.countryCode" label="Pays (code)" />
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<UiButton
|
|
||||||
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
|
||||||
type="submit"
|
|
||||||
:disabled="isLoading"
|
|
||||||
@click="submitted = true"
|
|
||||||
>
|
|
||||||
Valider
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { AddressPayload } from "~/services/address"
|
|
||||||
import { getCommunesByPostalCode, type CommuneData } from "~/services/geo"
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
type?: "supplier" | "customer",
|
|
||||||
address?: AddressPayload | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const submitted = ref(false)
|
|
||||||
const communes = ref<CommuneData[]>([])
|
|
||||||
const isLoadingCities = ref(false)
|
|
||||||
|
|
||||||
const communeOptions = computed(() =>
|
|
||||||
communes.value.map(c => ({ value: c.nom, label: c.nom }))
|
|
||||||
)
|
|
||||||
|
|
||||||
const emptyForm = (): AddressPayload => ({
|
|
||||||
street: "",
|
|
||||||
street2: null,
|
|
||||||
postalCode: "",
|
|
||||||
city: "",
|
|
||||||
countryCode: "FR",
|
|
||||||
})
|
|
||||||
|
|
||||||
const form = reactive<AddressPayload>(emptyForm())
|
|
||||||
|
|
||||||
const backPath = computed(() => {
|
|
||||||
if (props.type === "customer") {
|
|
||||||
const customerId = Number(route.query.customerId)
|
|
||||||
return Number.isFinite(customerId) && customerId > 0
|
|
||||||
? `/admin/customer/${customerId}`
|
|
||||||
: "/admin/customer/customer-list"
|
|
||||||
}
|
|
||||||
|
|
||||||
const supplierId = Number(route.query.supplierId)
|
|
||||||
return Number.isFinite(supplierId) && supplierId > 0
|
|
||||||
? `/admin/supplier/${supplierId}`
|
|
||||||
: "/admin/supplier/supplier-list"
|
|
||||||
})
|
|
||||||
|
|
||||||
const hydrateForm = (address?: AddressPayload | null) => {
|
|
||||||
const data = address ?? emptyForm()
|
|
||||||
form.street = data.street ?? ""
|
|
||||||
form.street2 = data.street2 ?? null
|
|
||||||
form.postalCode = data.postalCode ?? ""
|
|
||||||
form.city = data.city ?? ""
|
|
||||||
form.countryCode = data.countryCode || "FR"
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.address,
|
|
||||||
(addr) => {
|
|
||||||
hydrateForm(addr)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => form.postalCode,
|
|
||||||
(cp) => {
|
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
|
||||||
|
|
||||||
if (!cp || cp.length < 5) {
|
|
||||||
communes.value = []
|
|
||||||
form.city = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cp.length === 5) {
|
|
||||||
debounceTimer = setTimeout(async () => {
|
|
||||||
isLoadingCities.value = true
|
|
||||||
const previousCity = form.city
|
|
||||||
try {
|
|
||||||
communes.value = await getCommunesByPostalCode(cp)
|
|
||||||
|
|
||||||
if (communes.value.length === 1) {
|
|
||||||
form.city = communes.value[0].nom
|
|
||||||
} else if (communes.value.some(c => c.nom === previousCity)) {
|
|
||||||
form.city = previousCity
|
|
||||||
} else {
|
|
||||||
form.city = ''
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoadingCities.value = false
|
|
||||||
}
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const validateForm = () => {
|
|
||||||
if (isLoading.value) return
|
|
||||||
emit("validate", {...form})
|
|
||||||
}
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
router.push(backPath.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'validate', form: AddressPayload): void
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
<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>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form>
|
|
||||||
<div class="grid grid-cols-3 gap-x-40 gap-y-8 mb-8">
|
|
||||||
<UiNumberInput
|
|
||||||
:key="localWeight.type"
|
|
||||||
:label="'POIDS'"
|
|
||||||
labelClass="font-bold uppercase text-xl "
|
|
||||||
v-model="localWeight.weight"
|
|
||||||
:disabled="!isAdmin"
|
|
||||||
:min="0"
|
|
||||||
wrapper-class="flex-col"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UiDateInput
|
|
||||||
label="Date de pesée"
|
|
||||||
v-model="localWeight.weighedAt"
|
|
||||||
:disabled="!isAdmin"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UiNumberInput
|
|
||||||
label="Dsd"
|
|
||||||
class="col-start-2"
|
|
||||||
labelClass="font-bold uppercase"
|
|
||||||
v-model="localWeight.dsd"
|
|
||||||
:disabled="!isAdmin"
|
|
||||||
wrapper-class="flex-col"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {WeightEntryData} from '~/services/dto/weight-data'
|
|
||||||
import {reactive, watch} from "vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: WeightEntryData
|
|
||||||
isAdmin: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:modelValue', value: WeightEntryData): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const localWeight = reactive<WeightEntryData>({...props.modelValue})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(value) => {
|
|
||||||
Object.assign(localWeight, value)
|
|
||||||
},
|
|
||||||
{deep: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
localWeight,
|
|
||||||
(value) => {
|
|
||||||
emit('update:modelValue', {...value})
|
|
||||||
},
|
|
||||||
{deep: true}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UiModal v-model="open" title="Exporter l'inventaire bovin" max-width="max-w-2xl">
|
|
||||||
<p class="mb-5 text-sm text-slate-600">
|
|
||||||
Aucun filtre coché : export complet (tous les bovins actifs).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="mb-5">
|
|
||||||
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-600">
|
|
||||||
Tranches d'âge
|
|
||||||
</h3>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label
|
|
||||||
v-for="bucket in ageBuckets"
|
|
||||||
:key="bucket.value"
|
|
||||||
class="flex items-center gap-3 cursor-pointer text-primary-700"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="filters.ageRanges"
|
|
||||||
type="checkbox"
|
|
||||||
:value="bucket.value"
|
|
||||||
class="h-4 w-4 cursor-pointer accent-primary-500"
|
|
||||||
/>
|
|
||||||
<span :class="['inline-block rounded px-2 py-0.5 text-xs font-semibold text-white', bucket.colorClass]">
|
|
||||||
{{ bucket.badge }}
|
|
||||||
</span>
|
|
||||||
<span>{{ bucket.label }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:disabled="loading"
|
|
||||||
class="inline-flex h-[50px] items-center justify-center gap-2 rounded bg-primary-500 px-6 text-base text-white uppercase hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
@click="onSubmit"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
v-if="loading"
|
|
||||||
name="mdi:loading"
|
|
||||||
size="20"
|
|
||||||
class="animate-spin"
|
|
||||||
/>
|
|
||||||
<Icon v-else name="mdi:file-excel-outline" size="20" />
|
|
||||||
Exporter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UiModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, reactive, watch } from 'vue'
|
|
||||||
|
|
||||||
export interface InventoryExportFilters {
|
|
||||||
ageRanges: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
loading?: boolean
|
|
||||||
}>(), {
|
|
||||||
loading: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: boolean): void
|
|
||||||
(e: 'submit', filters: InventoryExportFilters): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const open = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value: boolean) => emit('update:modelValue', value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ageBuckets = [
|
|
||||||
{ value: 'over24', label: '≥ 24 mois', badge: '24+', colorClass: 'bg-red-500' },
|
|
||||||
{ value: 'between22And24', label: '22 à 24 mois', badge: '22-24', colorClass: 'bg-orange-500' },
|
|
||||||
{ value: 'between20And22', label: '20 à 22 mois', badge: '20-22', colorClass: 'bg-yellow-500' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const filters = reactive<InventoryExportFilters>({
|
|
||||||
ageRanges: []
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(open, (isOpen) => {
|
|
||||||
if (isOpen) {
|
|
||||||
filters.ageRanges = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = () => {
|
|
||||||
emit('submit', { ageRanges: [...filters.ageRanges] })
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form
|
|
||||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
|
||||||
class="flex flex-col gap-16"
|
|
||||||
@submit.prevent="goNext"
|
|
||||||
>
|
|
||||||
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
|
|
||||||
<div
|
|
||||||
class="flex flex-row gap-8 items-center w-full">
|
|
||||||
<div
|
|
||||||
v-for="type in bovineType"
|
|
||||||
:key="type.id"
|
|
||||||
class="mt-8 flex flex-row mb-2 w-full">
|
|
||||||
<UiNumberInput
|
|
||||||
:id="type.id"
|
|
||||||
:label="type.label"
|
|
||||||
:code="type.code"
|
|
||||||
v-model="bovineQuantities[String(type.id)]"
|
|
||||||
:placeholder="0"
|
|
||||||
:min="0"
|
|
||||||
:max="10"
|
|
||||||
class="max-w-[150px]"
|
|
||||||
wrapper-class="gap-3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mt-8 flex flex-row mb-2 gap-6">
|
|
||||||
<UiNumberInput
|
|
||||||
label="Autres"
|
|
||||||
v-model="otherQuantity"
|
|
||||||
class="max-w-[80px]"
|
|
||||||
wrapper-class="gap-3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-red-500 text-sm" :class="showBovineError ? '' : 'invisible'">
|
|
||||||
Veuillez saisir au moins une race bovine.
|
|
||||||
</p>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<UiButton
|
|
||||||
type="submit"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
|
||||||
>Valider
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type {BovineTypeData} from "~/services/dto/bovine-type-data";
|
|
||||||
import {getBovineTypeList} from "~/services/bovine-type";
|
|
||||||
import {RECEPTION_TYPE_CODES} from "~/utils/constants";
|
|
||||||
import {useReceptionStore} from '~/stores/reception'
|
|
||||||
import {
|
|
||||||
createReceptionBovine,
|
|
||||||
deleteReceptionBovine,
|
|
||||||
getReceptionBovineList,
|
|
||||||
updateReceptionBovine
|
|
||||||
} from "~/services/reception-bovine";
|
|
||||||
import {computed, onMounted, reactive, ref, watch} from "vue";
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const isLoadingBovineType = ref(false)
|
|
||||||
const bovineType = ref<BovineTypeData[]>([])
|
|
||||||
const receptionStore = useReceptionStore()
|
|
||||||
const showBovineError = ref(false)
|
|
||||||
const bovineQuantities = reactive<Record<string, number | null>>({})
|
|
||||||
const otherQuantity = ref<number | null>(0)
|
|
||||||
const receptionId = computed(() => receptionStore.current?.id ?? null)
|
|
||||||
const receptionIri = computed(() =>
|
|
||||||
receptionId.value ? `/api/receptions/${receptionId.value}` : null
|
|
||||||
)
|
|
||||||
const totalBovines = computed(() => {
|
|
||||||
const base = Object.values(bovineQuantities).reduce((sum, value) => {
|
|
||||||
return sum + (value ?? 0)
|
|
||||||
}, 0)
|
|
||||||
return base + (otherQuantity.value ?? 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadBovineType = async () => {
|
|
||||||
isLoadingBovineType.value = true
|
|
||||||
try {
|
|
||||||
bovineType.value = await getBovineTypeList()
|
|
||||||
} finally {
|
|
||||||
isLoadingBovineType.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadBovineType()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
[() => receptionId.value, () => bovineType.value],
|
|
||||||
async ([id, types]) => {
|
|
||||||
if (!id || !receptionIri.value || types.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectionMap: Record<string, number | null> = {}
|
|
||||||
for (const type of types) {
|
|
||||||
selectionMap[String(type.id)] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = await getReceptionBovineList(receptionIri.value)
|
|
||||||
for (const selection of existing) {
|
|
||||||
const bovineTypeId = String(selection.bovineType.id)
|
|
||||||
selectionMap[bovineTypeId] = selection.quantity ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of Object.keys(bovineQuantities)) {
|
|
||||||
delete bovineQuantities[key]
|
|
||||||
}
|
|
||||||
Object.assign(bovineQuantities, selectionMap)
|
|
||||||
|
|
||||||
const existingOther = receptionStore.current?.bovineDetail
|
|
||||||
const parsedOther =
|
|
||||||
typeof existingOther === 'string' && existingOther.trim() !== ''
|
|
||||||
? Number(existingOther)
|
|
||||||
: 0
|
|
||||||
otherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function syncBovineSelections(receptionIri: string) {
|
|
||||||
const existing = await getReceptionBovineList(receptionIri)
|
|
||||||
const existingMap = new Map<string, { id: number; quantity: number | null }>()
|
|
||||||
|
|
||||||
for (const selection of existing) {
|
|
||||||
const bovineTypeId = String(selection.bovineType.id)
|
|
||||||
existingMap.set(bovineTypeId, {
|
|
||||||
id: selection.id,
|
|
||||||
quantity: selection.quantity ?? 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supprime les entrées supprimées ou modifiées
|
|
||||||
for (const [bovineTypeId, entry] of existingMap.entries()) {
|
|
||||||
const selectedQuantity = bovineQuantities[bovineTypeId] ?? 0
|
|
||||||
if (!selectedQuantity) {
|
|
||||||
await deleteReceptionBovine(entry.id)
|
|
||||||
existingMap.delete(bovineTypeId)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedQuantity !== entry.quantity) {
|
|
||||||
await updateReceptionBovine(entry.id, {quantity: selectedQuantity})
|
|
||||||
existingMap.set(bovineTypeId, {
|
|
||||||
id: entry.id,
|
|
||||||
quantity: selectedQuantity
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crée les entrées manquantes
|
|
||||||
for (const [bovineTypeId, quantity] of Object.entries(bovineQuantities)) {
|
|
||||||
if (!quantity) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (existingMap.has(bovineTypeId)) {
|
|
||||||
// Déjà à jour
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await createReceptionBovine({
|
|
||||||
reception: receptionIri,
|
|
||||||
bovineType: `/api/bovine_types/${bovineTypeId}`,
|
|
||||||
quantity
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function goNext() {
|
|
||||||
if (!receptionStore.current || !receptionIri.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showBovineError.value = false
|
|
||||||
|
|
||||||
if (totalBovines.value === 0) {
|
|
||||||
showBovineError.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalBovines.value > 52) {
|
|
||||||
toast.error({
|
|
||||||
title: 'Erreur',
|
|
||||||
message: ('Le total des bovins ne peut pas dépasser 52.')
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = receptionStore.current.currentStep + 1
|
|
||||||
await syncBovineSelections(receptionIri.value)
|
|
||||||
|
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
merchandiseType: null,
|
|
||||||
merchandiseDetail: null,
|
|
||||||
bovineDetail: otherQuantity.value ? String(otherQuantity.value) : null,
|
|
||||||
currentStep: nextStep
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,284 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<form ref="formRef" :class="{ submitted }" @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
<div class="grid grid-cols-1 items-start gap-8 mb-16">
|
||||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Réception</h1>
|
<h1 class="font-bold text-5xl uppercase">Réception</h1>
|
||||||
<UiSelect
|
<div>
|
||||||
id="reception-user"
|
|
||||||
v-model="form.userId"
|
|
||||||
label="Nom de l'utilisateur"
|
|
||||||
:options="users.map((user) => ({
|
|
||||||
value: String(user.id),
|
|
||||||
label: user.username
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingUsers"
|
|
||||||
wrapper-class="col-start-1 row-start-2"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiDateInput
|
|
||||||
id="reception-date"
|
|
||||||
v-model="form.receptionDate"
|
|
||||||
label="Date de réception"
|
|
||||||
wrapper-class="col-start-1 row-start-3"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="reception-type"
|
|
||||||
v-model="form.receptionTypeId"
|
|
||||||
label="Type de réception"
|
|
||||||
:options="receptionTypes.map((type) => ({
|
|
||||||
value: String(type.id),
|
|
||||||
label: type.label
|
|
||||||
}))"
|
|
||||||
wrapper-class="col-start-1 row-start-4"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="reception-supplier"
|
|
||||||
v-model="form.supplierId"
|
|
||||||
label="Fournisseur"
|
|
||||||
:options="suppliers.map((supplier) => ({
|
|
||||||
value: String(supplier.id),
|
|
||||||
label: supplier.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingSuppliers"
|
|
||||||
wrapper-class="col-start-1 row-start-5"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="reception-address"
|
|
||||||
v-model="form.addressId"
|
|
||||||
label="Adresse"
|
|
||||||
:options="addressOptions"
|
|
||||||
:disabled="isLoadingSuppliers || ownerAddresses.length === 0"
|
|
||||||
wrapper-class="col-start-2 row-start-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="reception-truck"
|
|
||||||
v-model="form.truckId"
|
|
||||||
label="Camion"
|
|
||||||
:options="trucks.map((truck) => ({
|
|
||||||
value: String(truck.id),
|
|
||||||
label: truck.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingTrucks"
|
|
||||||
wrapper-class="col-start-2 row-start-2"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="reception-carrier"
|
|
||||||
v-model="form.carrierId"
|
|
||||||
label="Transporteur"
|
|
||||||
:options="carriers.map((carrier) => ({
|
|
||||||
value: String(carrier.id),
|
|
||||||
label: carrier.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingCarriers"
|
|
||||||
select-class="h-[34px]"
|
|
||||||
wrapper-class="col-start-2 row-start-3"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UiSelect
|
<div class="flex flex-col">
|
||||||
v-if="isLiotCarrier"
|
<label for="reception-date" class="font-bold uppercase text-xl mb-4">Date de reception</label>
|
||||||
id="reception-vehicle"
|
<input
|
||||||
v-model="form.vehicleId"
|
id="reception-date"
|
||||||
label="Immatriculation"
|
v-model="form.receptionDate"
|
||||||
:options="filteredVehicles.map((vehicle) => ({
|
type="date"
|
||||||
value: String(vehicle.id),
|
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
||||||
label: vehicle.plate
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingVehicles"
|
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
|
||||||
wrapper-class="col-start-2 row-start-4 h-[64px]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="reception-driver"
|
|
||||||
v-model="form.driverId"
|
|
||||||
label="Nom du chauffeur si LIOT"
|
|
||||||
:options="filteredDrivers.map((driver) => ({
|
|
||||||
value: String(driver.id),
|
|
||||||
label: driver.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingDrivers"
|
|
||||||
v-if="isLiotCarrier"
|
|
||||||
wrapper-class="col-start-2 row-start-5"
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UiButton
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl mb-16 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"
|
||||||
@click="submitted = true"
|
>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 { useFormDataLoading } from '~/composables/useFormDataLoading'
|
|
||||||
import { useLiotHandling } from '~/composables/useLiotHandling'
|
type ReceptionFormData = {
|
||||||
import { useAddressSync } from '~/composables/useAddressSync'
|
licensePlate: string
|
||||||
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
receptionDate: string
|
||||||
import { getReceptionTypeList } from '~/services/reception-type'
|
}
|
||||||
import type { SupplierData } from '~/services/dto/supplier-data'
|
|
||||||
import { getSupplierList } from '~/services/supplier'
|
|
||||||
import { RECEPTION_TYPE_CODES } from '~/utils/constants'
|
|
||||||
import { deleteReceptionBovine, getReceptionBovineList } from '~/services/reception-bovine'
|
|
||||||
import type { ReceptionFormData } from '~/services/dto/reception-data'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const isHydrating = ref(false)
|
|
||||||
const submitted = ref(false)
|
|
||||||
const formRef = ref<HTMLFormElement | null>(null)
|
|
||||||
|
|
||||||
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 receptionTypes = ref<ReceptionTypeData[]>([])
|
|
||||||
const suppliers = ref<SupplierData[]>([])
|
|
||||||
const isLoadingSuppliers = ref(false)
|
|
||||||
|
|
||||||
const { users, trucks, carriers, isLoadingUsers, isLoadingTrucks, isLoadingCarriers, loadCommonData } =
|
|
||||||
useFormDataLoading(form)
|
|
||||||
|
|
||||||
const {
|
|
||||||
isLiotCarrier, filteredDrivers, filteredVehicles,
|
|
||||||
isLoadingDrivers, isLoadingVehicles, allowAnyLicensePlate,
|
|
||||||
loadDrivers, loadVehicles
|
|
||||||
} = useLiotHandling(form, carriers, isHydrating)
|
|
||||||
|
|
||||||
const supplierIdRef = computed(() => form.supplierId)
|
|
||||||
const { ownerAddresses, addressOptions } = useAddressSync(form, supplierIdRef, suppliers)
|
|
||||||
|
|
||||||
const selectedReceptionType = computed(() =>
|
|
||||||
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
|
||||||
)
|
|
||||||
|
|
||||||
const clearReceptionBovines = async (receptionIri: string) => {
|
|
||||||
const existing = await getReceptionBovineList(receptionIri)
|
|
||||||
for (const selection of existing) {
|
|
||||||
await deleteReceptionBovine(selection.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSuppliers = async () => {
|
|
||||||
isLoadingSuppliers.value = true
|
|
||||||
try {
|
|
||||||
suppliers.value = await getSupplierList()
|
|
||||||
} finally {
|
|
||||||
isLoadingSuppliers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => receptionStore.current,
|
() => receptionStore.current,
|
||||||
(reception) => {
|
(reception) => {
|
||||||
isHydrating.value = true
|
|
||||||
form.licensePlate = reception?.licensePlate ?? ''
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
form.receptionDate = reception?.receptionDate?.slice(0, 10) ?? 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 }
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
async function validate() {
|
||||||
receptionTypes.value = await getReceptionTypeList()
|
|
||||||
await loadSuppliers()
|
|
||||||
await loadCommonData()
|
|
||||||
await loadDrivers()
|
|
||||||
await loadVehicles()
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildPayload = () => {
|
|
||||||
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
|
|
||||||
|
|
||||||
return {
|
|
||||||
licensePlate: normalizedLicensePlate,
|
|
||||||
receptionDate: normalizedReceptionDate,
|
|
||||||
receptionType: receptionTypeIri,
|
|
||||||
user: userIri,
|
|
||||||
supplier: supplierIri,
|
|
||||||
address: addressIri,
|
|
||||||
truck: truckIri,
|
|
||||||
carrier: carrierIri,
|
|
||||||
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveDraft = async () => {
|
|
||||||
const payload = buildPayload()
|
|
||||||
if (!receptionStore.current) {
|
|
||||||
await receptionStore.createReception({
|
|
||||||
currentStep: 0,
|
|
||||||
...payload
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
currentStep: receptionStore.current.currentStep,
|
|
||||||
...payload
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateFields = () => {
|
|
||||||
submitted.value = true
|
|
||||||
return formRef.value?.reportValidity() ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ saveDraft, validateFields })
|
|
||||||
|
|
||||||
async function validate() {
|
|
||||||
const payload = buildPayload()
|
|
||||||
|
|
||||||
if (!receptionStore.current) {
|
if (!receptionStore.current) {
|
||||||
const created = await receptionStore.createReception({
|
const created = await receptionStore.createReception({
|
||||||
currentStep: 1,
|
currentStep: 1,
|
||||||
...payload
|
licensePlate: normalizedLicensePlate,
|
||||||
|
receptionDate: normalizedReceptionDate
|
||||||
})
|
})
|
||||||
if (created) {
|
if (created) {
|
||||||
await router.push(`/reception/${created.id}`)
|
await router.push(`/reception/${created.id}`)
|
||||||
@@ -286,20 +69,12 @@ 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,
|
||||||
...payload
|
licensePlate: normalizedLicensePlate,
|
||||||
|
receptionDate: normalizedReceptionDate
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,288 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form :class="['flex flex-col items-center gap-16', { submitted }]" @submit.prevent="goNext">
|
|
||||||
<div
|
|
||||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
|
||||||
class="flex flex-col gap-16 items-center w-full">
|
|
||||||
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des marchandises réceptionnnées</h1>
|
|
||||||
<UiSelect
|
|
||||||
id="merchandise-type"
|
|
||||||
v-model="selectedMerchandiseTypeId"
|
|
||||||
label="Type de marchandises"
|
|
||||||
:options="merchandiseTypes.map((type) => ({ value: String(type.id), label: type.label }))"
|
|
||||||
wrapper-class="w-[550px]"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="selectedMerchandiseTypeId && isAutres"
|
|
||||||
class="flex flex-col w-full max-w-[550px]"
|
|
||||||
>
|
|
||||||
<UiTextInput
|
|
||||||
id="merchandise-detail"
|
|
||||||
v-model="merchandiseDetail"
|
|
||||||
label="Préciser"
|
|
||||||
placeholder="Précisions complémentaires"
|
|
||||||
:maxlength="255"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
|
||||||
class="flex flex-col gap-4 w-[550px]"
|
|
||||||
>
|
|
||||||
<div class="flex gap-4 justify-between">
|
|
||||||
<div
|
|
||||||
v-for="building in buildings"
|
|
||||||
:key="building.id"
|
|
||||||
>
|
|
||||||
<UiCheckbox
|
|
||||||
v-model="selectedBuildingIds"
|
|
||||||
:value="String(building.id)"
|
|
||||||
:label="building.label"
|
|
||||||
label-class="text-xl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-red-500 text-sm" :class="showBuildingError ? '' : 'invisible'">
|
|
||||||
Veuillez sélectionner au moins un bâtiment.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="selectedMerchandiseTypeId && isGranule"
|
|
||||||
class="flex flex-col gap-10 w-full max-w-[1100px]"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
|
||||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
|
||||||
<p class="font-bold uppercase text-primary-500">{{ type.label }}</p>
|
|
||||||
<div
|
|
||||||
v-for="building in buildings"
|
|
||||||
:key="building.id"
|
|
||||||
class="flex items-center gap-2 text-lg pl-[2px]"
|
|
||||||
>
|
|
||||||
<UiCheckbox
|
|
||||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
|
||||||
:value="String(building.id)"
|
|
||||||
:label="building.label"
|
|
||||||
label-class="text-xl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-red-500 text-sm" :class="showBuildingError ? '' : 'invisible'">
|
|
||||||
Veuillez sélectionner au moins un bâtiment.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<UiButton
|
|
||||||
type="submit"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
|
||||||
@click="submitted = true"
|
|
||||||
>Valider
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {computed, onMounted, ref} from 'vue'
|
|
||||||
import {getBuildingList} from '~/services/building'
|
|
||||||
import {getMerchandiseTypeList} from '~/services/merchandise-type'
|
|
||||||
import type {MerchandiseTypeData} from '~/services/dto/merchandise-type-data'
|
|
||||||
import type {BuildingData} from '~/services/dto/building-data'
|
|
||||||
import type {PelletTypeData} from '~/services/dto/pellet-type-data'
|
|
||||||
import {getPelletTypeList} from '~/services/pellet-type'
|
|
||||||
import {
|
|
||||||
createReceptionPelletBuilding,
|
|
||||||
deleteReceptionPelletBuilding,
|
|
||||||
getReceptionPelletBuildingList
|
|
||||||
} from '~/services/reception-pellet-building'
|
|
||||||
import {useReceptionStore} from '~/stores/reception'
|
|
||||||
import {MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES} from '~/utils/constants'
|
|
||||||
import ReceptionBovineReceived from "~/components/reception/reception-bovine-received.vue";
|
|
||||||
|
|
||||||
const receptionStore = useReceptionStore()
|
|
||||||
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
|
||||||
const buildings = ref<BuildingData[]>([])
|
|
||||||
const pelletTypes = ref<PelletTypeData[]>([])
|
|
||||||
const selectedMerchandiseTypeId = ref('')
|
|
||||||
const selectedBuildingIds = ref<string[]>([])
|
|
||||||
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
|
||||||
const merchandiseDetail = ref('')
|
|
||||||
const submitted = ref(false)
|
|
||||||
const showBuildingError = ref(false)
|
|
||||||
const showPelletBuildingError = ref(false)
|
|
||||||
|
|
||||||
// Extrait l'ID d'une relation depuis un IRI ou un objet complet.
|
|
||||||
const getRelationId = (value: unknown): string | null => {
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const match = value.match(/\/(\d+)$/)
|
|
||||||
return match ? match[1] : null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object' && 'id' in value) {
|
|
||||||
const record = value as { id?: number | string }
|
|
||||||
if (typeof record.id === 'number') {
|
|
||||||
return String(record.id)
|
|
||||||
}
|
|
||||||
if (typeof record.id === 'string') {
|
|
||||||
return record.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type de marchandise sélectionné dans le select
|
|
||||||
const selectedMerchandiseType = computed(() =>
|
|
||||||
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value)
|
|
||||||
)
|
|
||||||
// Indique si le type est "Granulé"
|
|
||||||
const isGranule = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE)
|
|
||||||
// Indique si le type est "Autres"
|
|
||||||
const isAutres = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES)
|
|
||||||
|
|
||||||
// Charge les référentiels et hydrate le formulaire depuis la réception
|
|
||||||
onMounted(async () => {
|
|
||||||
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
|
||||||
getMerchandiseTypeList(),
|
|
||||||
getBuildingList(),
|
|
||||||
getPelletTypeList()
|
|
||||||
])
|
|
||||||
merchandiseTypes.value = merchandiseTypeList
|
|
||||||
buildings.value = buildingList
|
|
||||||
pelletTypes.value = pelletTypeList
|
|
||||||
|
|
||||||
const currentId = receptionStore.current?.merchandiseType?.id
|
|
||||||
if (currentId) {
|
|
||||||
selectedMerchandiseTypeId.value = String(currentId)
|
|
||||||
}
|
|
||||||
merchandiseDetail.value = receptionStore.current?.merchandiseDetail ?? ''
|
|
||||||
|
|
||||||
selectedBuildingIds.value =
|
|
||||||
receptionStore.current?.buildings?.map((building) => String(building.id)) ?? []
|
|
||||||
|
|
||||||
const existingPelletSelections = receptionStore.current?.pelletBuildings ?? []
|
|
||||||
const selectionMap: Record<string, string[]> = {}
|
|
||||||
for (const selection of existingPelletSelections) {
|
|
||||||
// L'API peut renvoyer les relations comme IRI ou comme objets selon le contexte.
|
|
||||||
const pelletTypeId = getRelationId(selection.pelletType)
|
|
||||||
const buildingId = getRelationId(selection.building)
|
|
||||||
if (!pelletTypeId || !buildingId) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!selectionMap[pelletTypeId]) {
|
|
||||||
selectionMap[pelletTypeId] = []
|
|
||||||
}
|
|
||||||
selectionMap[pelletTypeId].push(buildingId)
|
|
||||||
}
|
|
||||||
for (const pelletType of pelletTypes.value) {
|
|
||||||
const key = String(pelletType.id)
|
|
||||||
if (!selectionMap[key]) {
|
|
||||||
selectionMap[key] = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedPelletBuildingIds.value = selectionMap
|
|
||||||
})
|
|
||||||
// Enregistre les sélections et passe à l'étape suivante
|
|
||||||
async function goNext() {
|
|
||||||
if (!receptionStore.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showBuildingError.value = false
|
|
||||||
showPelletBuildingError.value = false
|
|
||||||
|
|
||||||
if (!isGranule.value && !isAutres.value && selectedBuildingIds.value.length === 0) {
|
|
||||||
showBuildingError.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGranule.value) {
|
|
||||||
const hasAnyPelletBuilding = Object.values(selectedPelletBuildingIds.value)
|
|
||||||
.some((ids) => ids.length > 0)
|
|
||||||
if (!hasAnyPelletBuilding) {
|
|
||||||
showPelletBuildingError.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = receptionStore.current.currentStep + 1
|
|
||||||
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
|
||||||
|
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
merchandiseType: selectedMerchandiseTypeId.value
|
|
||||||
? `/api/merchandise_types/${selectedMerchandiseTypeId.value}`
|
|
||||||
: null,
|
|
||||||
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() : null,
|
|
||||||
buildings: isGranule.value
|
|
||||||
? []
|
|
||||||
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
|
|
||||||
bovineDetail: null,
|
|
||||||
bovinesTypes: null,
|
|
||||||
currentStep: nextStep
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isGranule.value) {
|
|
||||||
await syncPelletSelections(receptionIri)
|
|
||||||
} else {
|
|
||||||
await clearPelletSelections(receptionIri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supprime toutes les associations granulés/bâtiments existantes
|
|
||||||
async function clearPelletSelections(receptionIri: string) {
|
|
||||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
|
||||||
for (const selection of existing) {
|
|
||||||
await deleteReceptionPelletBuilding(selection.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Synchronise les associations granulés/bâtiments avec l'état du formulaire
|
|
||||||
async function syncPelletSelections(receptionIri: string) {
|
|
||||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
|
||||||
const existingMap = new Map<string, number>()
|
|
||||||
for (const selection of existing) {
|
|
||||||
// Construit la table de correspondance avec des IDs normalisés pour éviter les doublons.
|
|
||||||
const pelletTypeId = getRelationId(selection.pelletType)
|
|
||||||
const buildingId = getRelationId(selection.building)
|
|
||||||
if (!pelletTypeId || !buildingId) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const key = `${pelletTypeId}:${buildingId}`
|
|
||||||
existingMap.set(key, selection.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
|
|
||||||
for (const [pelletTypeId, buildingIds] of Object.entries(selectedPelletBuildingIds.value)) {
|
|
||||||
for (const buildingId of buildingIds) {
|
|
||||||
desiredEntries.push({pelletTypeId, buildingId})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const desiredKeys = new Set(desiredEntries.map(
|
|
||||||
(entry) => `${entry.pelletTypeId}:${entry.buildingId}`
|
|
||||||
))
|
|
||||||
|
|
||||||
for (const [key, id] of existingMap.entries()) {
|
|
||||||
if (!desiredKeys.has(key)) {
|
|
||||||
await deleteReceptionPelletBuilding(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of desiredEntries) {
|
|
||||||
const key = `${entry.pelletTypeId}:${entry.buildingId}`
|
|
||||||
if (!existingMap.has(key)) {
|
|
||||||
await createReceptionPelletBuilding({
|
|
||||||
reception: receptionIri,
|
|
||||||
pelletType: `/api/pellet_types/${entry.pelletTypeId}`,
|
|
||||||
building: `/api/buildings/${entry.buildingId}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
30
frontend/components/reception/reception-unloading.vue
Normal file
30
frontend/components/reception/reception-unloading.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<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>
|
||||||
104
frontend/components/reception/reception-weight.vue
Normal file
104
frontend/components/reception/reception-weight.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase">{{ 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="showLoadingBox"
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="displayWeight !== null" class="w-full">
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
||||||
|
{{ displayWeight }} kg
|
||||||
|
</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 class="flex justify-center mt-[54px]">
|
||||||
|
<button
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
@click="fetchWeight"
|
||||||
|
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
||||||
|
<button
|
||||||
|
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</button>
|
||||||
|
<button
|
||||||
|
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</button>
|
||||||
|
</div>
|
||||||
|
<UiPdfPrinter ref="pdfPrinter" />
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useWeighing } from '~/composables/useWeighing'
|
||||||
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const receptionStore = useReceptionStore()
|
||||||
|
const { current: storeReception } = storeToRefs(receptionStore)
|
||||||
|
type PdfPrinterHandle = {
|
||||||
|
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 {
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
} = useWeighing({
|
||||||
|
mode: props.mode,
|
||||||
|
reception: storeReception,
|
||||||
|
updateReception: receptionStore.updateReception,
|
||||||
|
loadReception: receptionStore.loadReception
|
||||||
|
})
|
||||||
|
const showGenerateReceipt = computed(
|
||||||
|
() => props.mode === 'tare' && displayWeight.value !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
const printReceipt = async () => {
|
||||||
|
if (!import.meta.client || !receptionStore.current || !pdfPrinter.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWeight()
|
||||||
|
await pdfPrinter.value.print(`/receptions/${receptionStore.current.id}/receipt`)
|
||||||
|
|
||||||
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
const result = await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
|
isValid: true
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
receptionStore.clearCurrent()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchWeight()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="w-full relative grid grid-cols-[1fr_200px]">
|
|
||||||
<UiRadioGroup
|
|
||||||
id="merchandise-type"
|
|
||||||
v-model="selectedMerchandiseTypeId"
|
|
||||||
label="Type de marchandises"
|
|
||||||
:options="merchandiseTypes.map((type) => ({
|
|
||||||
value: String(type.id),
|
|
||||||
label: type.label
|
|
||||||
}))"
|
|
||||||
input-class="accent-primary-700 focus:ring-primary-700"
|
|
||||||
option-label-class="uppercase"
|
|
||||||
wrapper-class="w-full uppercase"
|
|
||||||
group-class="grid grid-cols-4 mt-9 mb-7"
|
|
||||||
:disabled="!isAdmin"
|
|
||||||
/>
|
|
||||||
<UiTextInput
|
|
||||||
v-if="isAutres"
|
|
||||||
id="merchandise-detail"
|
|
||||||
:disabled="!isAdmin"
|
|
||||||
v-model="merchandiseDetail"
|
|
||||||
placeholder="Préciser"
|
|
||||||
:maxlength="255"
|
|
||||||
wrapper-class="w-[200px] mt-12 mb-7"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
|
||||||
class="w-full grid grid-cols-[1fr_200px]"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-4 gap-6"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="building in buildings"
|
|
||||||
:key="building.id"
|
|
||||||
>
|
|
||||||
<UiCheckbox
|
|
||||||
v-model="selectedBuildingIds"
|
|
||||||
:value="String(building.id)"
|
|
||||||
:label="building.label"
|
|
||||||
:disabled="!isAdmin"
|
|
||||||
input-class="accent-primary-700 focus:ring-primary-700"
|
|
||||||
label-class="uppercase"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="selectedMerchandiseTypeId && isGranule"
|
|
||||||
class="grid grid-cols-[1fr_200px] w-full col-start-2 row-start-1"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-4 gap-6 justify-between">
|
|
||||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
|
||||||
<p class="mb-1 font-medium uppercase">{{ type.label }}</p>
|
|
||||||
<div
|
|
||||||
v-for="building in buildings"
|
|
||||||
:key="building.id"
|
|
||||||
class="flex text-lg"
|
|
||||||
>
|
|
||||||
<UiCheckbox
|
|
||||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
|
||||||
:value="String(building.id)"
|
|
||||||
:label="building.label"
|
|
||||||
:disabled="!isAdmin"
|
|
||||||
input-class="accent-primary-700 focus:ring-primary-700"
|
|
||||||
label-class="text-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
|
||||||
import type { BuildingData } from '~/services/dto/building-data'
|
|
||||||
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
|
||||||
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
|
||||||
import type { MerchandiseEntryData } from '~/services/dto/reception-data'
|
|
||||||
import { getBuildingList } from '~/services/building'
|
|
||||||
import { getMerchandiseTypeList } from '~/services/merchandise-type'
|
|
||||||
import { getPelletTypeList } from '~/services/pellet-type'
|
|
||||||
import { MERCHANDISE_TYPE_CODES } from '~/utils/constants'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: MerchandiseEntryData
|
|
||||||
isAdmin: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:modelValue', value: MerchandiseEntryData): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
|
||||||
const buildings = ref<BuildingData[]>([])
|
|
||||||
const pelletTypes = ref<PelletTypeData[]>([])
|
|
||||||
|
|
||||||
const selectedMerchandiseTypeId = ref('')
|
|
||||||
const selectedBuildingIds = ref<string[]>([])
|
|
||||||
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
|
||||||
const merchandiseDetail = ref('')
|
|
||||||
// Verrou de synchro pour empêcher les aller-retours infinis entre parent et composant.
|
|
||||||
const isSyncing = ref(false)
|
|
||||||
const isReady = ref(false)
|
|
||||||
|
|
||||||
const selectedMerchandiseType = computed(() =>
|
|
||||||
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value) ?? null
|
|
||||||
)
|
|
||||||
const isGranule = computed(
|
|
||||||
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE
|
|
||||||
)
|
|
||||||
const isAutres = computed(
|
|
||||||
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES
|
|
||||||
)
|
|
||||||
|
|
||||||
function clonePelletSelections(value: Record<string, string[]>) {
|
|
||||||
const clone: Record<string, string[]> = {}
|
|
||||||
for (const [key, buildingIds] of Object.entries(value)) {
|
|
||||||
clone[key] = [...buildingIds]
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
function sorted(values: string[]): string[] {
|
|
||||||
return [...values].sort()
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeModel(value: MerchandiseEntryData): MerchandiseEntryData {
|
|
||||||
// Normalisation stable pour comparer deux modèles sans faux positifs (ordre des tableaux).
|
|
||||||
const pellet: Record<string, string[]> = {}
|
|
||||||
const pelletKeys = Object.keys(value.selectedPelletBuildingIds ?? {}).sort()
|
|
||||||
for (const key of pelletKeys) {
|
|
||||||
pellet[key] = sorted(value.selectedPelletBuildingIds[key] ?? [])
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
merchandiseTypeId: value.merchandiseTypeId ?? '',
|
|
||||||
merchandiseDetail: value.merchandiseDetail ?? '',
|
|
||||||
selectedBuildingIds: sorted(value.selectedBuildingIds ?? []),
|
|
||||||
selectedPelletBuildingIds: pellet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCurrentModel(): MerchandiseEntryData {
|
|
||||||
return {
|
|
||||||
merchandiseTypeId: selectedMerchandiseTypeId.value,
|
|
||||||
merchandiseDetail: merchandiseDetail.value,
|
|
||||||
selectedBuildingIds: [...selectedBuildingIds.value],
|
|
||||||
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSameModel(left: MerchandiseEntryData, right: MerchandiseEntryData): boolean {
|
|
||||||
return JSON.stringify(normalizeModel(left)) === JSON.stringify(normalizeModel(right))
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensurePelletKeys() {
|
|
||||||
for (const pelletType of pelletTypes.value) {
|
|
||||||
const key = String(pelletType.id)
|
|
||||||
if (!selectedPelletBuildingIds.value[key]) {
|
|
||||||
selectedPelletBuildingIds.value[key] = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hydrateFromModelValue(value: MerchandiseEntryData) {
|
|
||||||
isSyncing.value = true
|
|
||||||
try {
|
|
||||||
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
|
|
||||||
merchandiseDetail.value = value.merchandiseDetail ?? ''
|
|
||||||
selectedBuildingIds.value = [...(value.selectedBuildingIds ?? [])]
|
|
||||||
selectedPelletBuildingIds.value = clonePelletSelections(
|
|
||||||
value.selectedPelletBuildingIds ?? {}
|
|
||||||
)
|
|
||||||
ensurePelletKeys()
|
|
||||||
} finally {
|
|
||||||
isSyncing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeLocalState() {
|
|
||||||
if (isGranule.value) {
|
|
||||||
if (selectedBuildingIds.value.length > 0) {
|
|
||||||
selectedBuildingIds.value = []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const key of Object.keys(selectedPelletBuildingIds.value)) {
|
|
||||||
if (selectedPelletBuildingIds.value[key].length > 0) {
|
|
||||||
selectedPelletBuildingIds.value[key] = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAutres.value && merchandiseDetail.value !== '') {
|
|
||||||
merchandiseDetail.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitCurrentModel() {
|
|
||||||
const currentModel = buildCurrentModel()
|
|
||||||
// Ne pas réémettre si rien n'a changé côté métier.
|
|
||||||
if (isSameModel(currentModel, props.modelValue)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('update:modelValue', currentModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(value) => {
|
|
||||||
const currentModel = buildCurrentModel()
|
|
||||||
// Si local == parent, on ignore pour éviter la boucle de réhydratation.
|
|
||||||
if (isSameModel(currentModel, value)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hydrateFromModelValue(value)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
|
|
||||||
() => {
|
|
||||||
if (isSyncing.value || !isReady.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeSanitize = buildCurrentModel()
|
|
||||||
isSyncing.value = true
|
|
||||||
// Applique les règles métier (granulé / autres) avant émission.
|
|
||||||
sanitizeLocalState()
|
|
||||||
isSyncing.value = false
|
|
||||||
|
|
||||||
const afterSanitize = buildCurrentModel()
|
|
||||||
// Si la sanitation a modifié l'état, on laisse le watcher repasser proprement.
|
|
||||||
if (!isSameModel(beforeSanitize, afterSanitize)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emitCurrentModel()
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
|
||||||
getMerchandiseTypeList(),
|
|
||||||
getBuildingList(),
|
|
||||||
getPelletTypeList()
|
|
||||||
])
|
|
||||||
merchandiseTypes.value = merchandiseTypeList
|
|
||||||
buildings.value = buildingList
|
|
||||||
pelletTypes.value = pelletTypeList
|
|
||||||
|
|
||||||
hydrateFromModelValue(props.modelValue)
|
|
||||||
isReady.value = true
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form ref="formRef" :class="{ submitted }" @submit.prevent="validate">
|
|
||||||
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
|
||||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Expédition</h1>
|
|
||||||
<UiSelect
|
|
||||||
id="shipment-user"
|
|
||||||
v-model="form.userId"
|
|
||||||
label="Nom de l'utilisateur"
|
|
||||||
:options="users.map((user) => ({
|
|
||||||
value: String(user.id),
|
|
||||||
label: user.username
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingUsers"
|
|
||||||
wrapper-class="col-start-1 row-start-2"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiDateInput
|
|
||||||
id="shipment-date"
|
|
||||||
v-model="form.shipmentDate"
|
|
||||||
label="Date du jour"
|
|
||||||
wrapper-class="col-start-1 row-start-3"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div class="col-start-1 row-start-4 h-[64px]">
|
|
||||||
<div class="flex w-full items-end gap-[104px]">
|
|
||||||
<UiRadioGroup
|
|
||||||
id="shipment-type"
|
|
||||||
name="shipment-type"
|
|
||||||
label="Type d'expédition bovine"
|
|
||||||
input-class="accent-primary-700 focus:ring-primary-700"
|
|
||||||
wrapper-class=""
|
|
||||||
group-class="flex flex-row gap-[104px] w-[160px_160px] h-[32px]"
|
|
||||||
v-model="selectedShipmentTypeId"
|
|
||||||
:options="bovineShipment.map((type) => ({
|
|
||||||
value: String(type.id),
|
|
||||||
label: type.label
|
|
||||||
}))"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiNumberInput
|
|
||||||
id="shipment-type-quantity"
|
|
||||||
v-model="shipmentQuantity"
|
|
||||||
:placeholder="0"
|
|
||||||
:min="0"
|
|
||||||
:max="1200"
|
|
||||||
:disabled="!selectedShipmentTypeId"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<UiSelect
|
|
||||||
id="shipment-customer"
|
|
||||||
v-model="form.customerId"
|
|
||||||
label="Client"
|
|
||||||
:options="customers.map((customer) => ({
|
|
||||||
value: String(customer.id),
|
|
||||||
label: customer.name || `Client #${customer.id}`
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingCustomers"
|
|
||||||
wrapper-class="col-start-1 row-start-5"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="shipment-address"
|
|
||||||
v-model="form.addressId"
|
|
||||||
:options="addressOptions"
|
|
||||||
:disabled="isLoadingCustomers || ownerAddresses.length === 0"
|
|
||||||
label="Adresse"
|
|
||||||
wrapper-class="col-start-2 row-start-1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="shipment-truck"
|
|
||||||
v-model="form.truckId"
|
|
||||||
label="Camion"
|
|
||||||
:options="trucks.map((truck) => ({
|
|
||||||
value: String(truck.id),
|
|
||||||
label: truck.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingTrucks"
|
|
||||||
wrapper-class="col-start-2 row-start-2"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="shipment-carrier"
|
|
||||||
v-model="form.carrierId"
|
|
||||||
label="Transporteur"
|
|
||||||
:options="carriers.map((carrier) => ({
|
|
||||||
value: String(carrier.id),
|
|
||||||
label: carrier.name
|
|
||||||
}))"
|
|
||||||
wrapper-class="col-start-2 row-start-3"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
|
||||||
<UiLicensePlateInput
|
|
||||||
v-model="form.licensePlate"
|
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<UiSelect
|
|
||||||
v-if="isLiotCarrier"
|
|
||||||
id="shipment-vehicle"
|
|
||||||
v-model="form.vehicleId"
|
|
||||||
label="Immatriculation"
|
|
||||||
:options="filteredVehicles.map((vehicle) => ({
|
|
||||||
value: String(vehicle.id),
|
|
||||||
label: vehicle.plate
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingVehicles"
|
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
|
||||||
wrapper-class="col-start-2 row-start-4"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
id="shipment-driver"
|
|
||||||
v-model="form.driverId"
|
|
||||||
label="Nom du chauffeur si LIOT"
|
|
||||||
:options="filteredDrivers.map((driver) => ({
|
|
||||||
value: String(driver.id),
|
|
||||||
label: driver.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingDrivers"
|
|
||||||
wrapper-class="col-start-2 row-start-5"
|
|
||||||
v-if="isLiotCarrier"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<UiButton
|
|
||||||
type="submit"
|
|
||||||
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
|
||||||
@click="submitted = true"
|
|
||||||
>Valider
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useFormDataLoading } from '~/composables/useFormDataLoading'
|
|
||||||
import { useLiotHandling } from '~/composables/useLiotHandling'
|
|
||||||
import { useAddressSync } from '~/composables/useAddressSync'
|
|
||||||
import type { CustomerData } from '~/services/dto/customer-data'
|
|
||||||
import { getCustomerList } from '~/services/customer'
|
|
||||||
import type { ShipmentFormData } from '~/services/dto/shipment-data'
|
|
||||||
import { useShipmentStore } from '~/stores/shipment'
|
|
||||||
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
|
||||||
import { getShipmentTypeList } from '~/services/shipment-type'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const shipmentStore = useShipmentStore()
|
|
||||||
const isHydrating = ref(false)
|
|
||||||
const submitted = ref(false)
|
|
||||||
const formRef = ref<HTMLFormElement | null>(null)
|
|
||||||
|
|
||||||
const form = reactive<ShipmentFormData>({
|
|
||||||
userId: '',
|
|
||||||
shipmentDate: new Date().toISOString().slice(0, 10),
|
|
||||||
customerId: '',
|
|
||||||
addressId: '',
|
|
||||||
truckId: '',
|
|
||||||
carrierId: '',
|
|
||||||
driverId: '',
|
|
||||||
vehicleId: '',
|
|
||||||
licensePlate: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const customers = ref<CustomerData[]>([])
|
|
||||||
const isLoadingCustomers = ref(false)
|
|
||||||
const bovineShipment = ref<ShipmentTypeData[]>([])
|
|
||||||
const selectedShipmentTypeId = ref('')
|
|
||||||
const shipmentQuantity = ref<number | null>(0)
|
|
||||||
|
|
||||||
const { users, trucks, carriers, isLoadingUsers, isLoadingTrucks, isLoadingCarriers, loadCommonData } =
|
|
||||||
useFormDataLoading(form)
|
|
||||||
|
|
||||||
const {
|
|
||||||
isLiotCarrier, filteredDrivers, filteredVehicles,
|
|
||||||
isLoadingDrivers, isLoadingVehicles, allowAnyLicensePlate,
|
|
||||||
loadDrivers, loadVehicles
|
|
||||||
} = useLiotHandling(form, carriers, isHydrating)
|
|
||||||
|
|
||||||
const customerIdRef = computed(() => form.customerId)
|
|
||||||
const { ownerAddresses, addressOptions } = useAddressSync(form, customerIdRef, customers)
|
|
||||||
|
|
||||||
const loadCustomers = async () => {
|
|
||||||
isLoadingCustomers.value = true
|
|
||||||
try {
|
|
||||||
customers.value = await getCustomerList()
|
|
||||||
} finally {
|
|
||||||
isLoadingCustomers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => shipmentStore.current,
|
|
||||||
(shipment) => {
|
|
||||||
isHydrating.value = true
|
|
||||||
form.licensePlate = shipment?.licensePlate ?? ''
|
|
||||||
form.shipmentDate = shipment?.shipmentDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
|
|
||||||
form.userId = shipment?.user?.id ? String(shipment.user.id) : form.userId
|
|
||||||
form.customerId = shipment?.customer?.id ? String(shipment.customer.id) : ''
|
|
||||||
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
|
|
||||||
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
|
|
||||||
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
|
|
||||||
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
|
|
||||||
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
|
|
||||||
selectedShipmentTypeId.value = shipment?.shipmentType?.id ? String(shipment.shipmentType.id) : ''
|
|
||||||
shipmentQuantity.value = shipment?.nbBovinSend ?? 0
|
|
||||||
isHydrating.value = false
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Extra watcher for LIOT defaults after hydration
|
|
||||||
watch(
|
|
||||||
() => isHydrating.value,
|
|
||||||
(value) => {
|
|
||||||
if (!value && isLiotCarrier.value) {
|
|
||||||
if (filteredDrivers.value.length === 1 && !form.driverId) {
|
|
||||||
form.driverId = String(filteredDrivers.value[0].id)
|
|
||||||
}
|
|
||||||
if (filteredVehicles.value.length === 1 && !form.vehicleId) {
|
|
||||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
bovineShipment.value = await getShipmentTypeList()
|
|
||||||
await loadCustomers()
|
|
||||||
await loadCommonData()
|
|
||||||
await loadVehicles()
|
|
||||||
await loadDrivers()
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildPayload = () => {
|
|
||||||
const normalizedLicensePlate = form.licensePlate.trim()
|
|
||||||
const normalizedShipmentDate = form.shipmentDate.trim()
|
|
||||||
const normalizedCustomerId = form.customerId.trim()
|
|
||||||
const normalizedTruckId = form.truckId.trim()
|
|
||||||
const normalizedCarrierId = form.carrierId.trim()
|
|
||||||
const normalizedDriverId = form.driverId.trim()
|
|
||||||
const normalizedUserId = form.userId.trim()
|
|
||||||
const normalizedAddressId = form.addressId.trim()
|
|
||||||
|
|
||||||
const customerIri = normalizedCustomerId ? `/api/customers/${normalizedCustomerId}` : null
|
|
||||||
const truckIri = normalizedTruckId ? `/api/trucks/${normalizedTruckId}` : null
|
|
||||||
const carrierIri = normalizedCarrierId ? `/api/carriers/${normalizedCarrierId}` : null
|
|
||||||
const userIri = normalizedUserId ? `/api/users/${normalizedUserId}` : null
|
|
||||||
const driverIri = normalizedDriverId ? `/api/drivers/${normalizedDriverId}` : null
|
|
||||||
const addressIri = normalizedAddressId ? `/api/addresses/${normalizedAddressId}` : null
|
|
||||||
const normalizedShipmentTypeId = selectedShipmentTypeId.value.trim()
|
|
||||||
const shipmentTypeIri = normalizedShipmentTypeId ? `/api/shipment_types/${normalizedShipmentTypeId}` : null
|
|
||||||
|
|
||||||
const rawQuantity = Number(shipmentQuantity.value ?? 0)
|
|
||||||
const normalizedQuantity = Number.isFinite(rawQuantity) ? Math.max(0, Math.trunc(rawQuantity)) : 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
licensePlate: normalizedLicensePlate,
|
|
||||||
shipmentDate: normalizedShipmentDate,
|
|
||||||
customer: customerIri,
|
|
||||||
truck: truckIri,
|
|
||||||
carrier: carrierIri,
|
|
||||||
driver: driverIri,
|
|
||||||
user: userIri,
|
|
||||||
address: addressIri,
|
|
||||||
shipmentType: shipmentTypeIri,
|
|
||||||
nbBovinSend: normalizedQuantity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveDraft = async () => {
|
|
||||||
const payload = buildPayload()
|
|
||||||
if (!shipmentStore.current) {
|
|
||||||
await shipmentStore.createShipment({
|
|
||||||
currentStep: 0,
|
|
||||||
...payload
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
|
||||||
currentStep: shipmentStore.current.currentStep,
|
|
||||||
...payload
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateFields = () => {
|
|
||||||
submitted.value = true
|
|
||||||
return formRef.value?.reportValidity() ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ saveDraft, validateFields })
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="relative border border-slate-200">
|
|
||||||
<div
|
|
||||||
class="grid items-center gap-6 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
|
||||||
:style="{ gridTemplateColumns: gridCols }"
|
|
||||||
>
|
|
||||||
<div v-for="col in columns" :key="col.key" class="min-w-0">
|
|
||||||
<slot :name="`header-${col.key}`" :column="col">{{ col.label }}</slot>
|
|
||||||
</div>
|
|
||||||
<div v-if="showActions" class="min-w-0">
|
|
||||||
<slot name="header-actions">Actions</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :class="dimRows ? 'opacity-50 transition-opacity' : ''" :aria-busy="loading || undefined">
|
|
||||||
<template v-if="paginatedItems.length">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in paginatedItems"
|
|
||||||
:key="item.id ?? index"
|
|
||||||
class="grid gap-6 px-4 py-3 text-sm border-t border-slate-200"
|
|
||||||
:class="[
|
|
||||||
rowClickable ? 'hover:bg-slate-50 cursor-pointer' : '',
|
|
||||||
rowClass ? rowClass(item) : ''
|
|
||||||
]"
|
|
||||||
:style="{ gridTemplateColumns: gridCols }"
|
|
||||||
:role="rowClickable ? 'button' : undefined"
|
|
||||||
:tabindex="rowClickable ? 0 : undefined"
|
|
||||||
@click="onRowClick(item)"
|
|
||||||
@keydown.enter="onRowClick(item)"
|
|
||||||
@keydown.space.prevent="onRowClick(item)"
|
|
||||||
>
|
|
||||||
<div v-for="col in columns" :key="col.key" class="min-w-0 truncate">
|
|
||||||
<slot :name="`cell-${col.key}`" :item="item" :column="col">
|
|
||||||
{{ getNestedValue(item, col.key) }}
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
<div v-if="showActions" @click.stop>
|
|
||||||
<slot name="actions" :item="item" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-else-if="loading"
|
|
||||||
class="flex items-center justify-center border-t border-slate-200 px-4 py-8 text-primary-500"
|
|
||||||
role="status"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<UiLoadingDots />
|
|
||||||
<span class="sr-only">Chargement…</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="border-t border-slate-200 px-4 py-8 text-center text-sm text-slate-500"
|
|
||||||
>
|
|
||||||
<slot name="empty">{{ emptyMessage }}</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="dimRows"
|
|
||||||
class="pointer-events-none absolute inset-0 flex items-center justify-center"
|
|
||||||
role="status"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<div class="rounded bg-white/80 px-4 py-2 text-primary-500 shadow">
|
|
||||||
<UiLoadingDots />
|
|
||||||
<span class="sr-only">Chargement…</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="total > 0" class="flex justify-between pt-2">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<label :for="perPageId" class="whitespace-nowrap text-sm text-slate-700">Lignes :</label>
|
|
||||||
<select
|
|
||||||
:id="perPageId"
|
|
||||||
:value="currentPerPage"
|
|
||||||
class="h-10 rounded border border-slate-300 bg-white px-2 text-sm text-primary-700"
|
|
||||||
@change="onPerPageChange(($event.target as HTMLSelectElement).value)"
|
|
||||||
>
|
|
||||||
<option v-for="n in perPageOptions" :key="n" :value="n">{{ n }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav aria-label="Pagination" class="flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="h-10 rounded border border-primary-500 bg-white px-3 text-sm text-primary-500 hover:bg-primary-500 hover:text-white disabled:cursor-not-allowed disabled:border-slate-300 disabled:text-slate-400 disabled:hover:bg-white disabled:hover:text-slate-400"
|
|
||||||
:disabled="currentPage <= 1"
|
|
||||||
aria-label="Page précédente"
|
|
||||||
@click="goToPage(currentPage - 1)"
|
|
||||||
>
|
|
||||||
Précédent
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<template v-for="(entry, i) in visiblePages" :key="`${typeof entry}-${entry}-${i}`">
|
|
||||||
<span
|
|
||||||
v-if="entry === '...'"
|
|
||||||
class="px-1 text-sm text-slate-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
>…</span>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
type="button"
|
|
||||||
class="h-10 min-w-[2.5rem] rounded px-2 text-sm transition-colors"
|
|
||||||
:class="entry === currentPage
|
|
||||||
? 'bg-primary-500 font-semibold text-white'
|
|
||||||
: 'text-slate-700 hover:bg-slate-100'"
|
|
||||||
:aria-current="entry === currentPage ? 'page' : undefined"
|
|
||||||
@click="goToPage(entry)"
|
|
||||||
>
|
|
||||||
{{ entry }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="h-10 rounded border border-primary-500 bg-white px-3 text-sm text-primary-500 hover:bg-primary-500 hover:text-white disabled:cursor-not-allowed disabled:border-slate-300 disabled:text-slate-400 disabled:hover:bg-white disabled:hover:text-slate-400"
|
|
||||||
:disabled="currentPage >= totalPages"
|
|
||||||
aria-label="Page suivante"
|
|
||||||
@click="goToPage(currentPage + 1)"
|
|
||||||
>
|
|
||||||
Suivant
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
||||||
import { computed, useId } from 'vue'
|
|
||||||
|
|
||||||
interface Column {
|
|
||||||
key: string
|
|
||||||
label: string
|
|
||||||
width?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
columns: Column[]
|
|
||||||
items: T[]
|
|
||||||
totalItems?: number
|
|
||||||
page?: number
|
|
||||||
perPage?: number
|
|
||||||
perPageOptions?: number[]
|
|
||||||
rowClickable?: boolean
|
|
||||||
showActions?: boolean
|
|
||||||
emptyMessage?: string
|
|
||||||
loading?: boolean
|
|
||||||
rowClass?: (item: T) => string | undefined
|
|
||||||
}>(), {
|
|
||||||
totalItems: undefined,
|
|
||||||
page: 1,
|
|
||||||
perPage: 10,
|
|
||||||
perPageOptions: () => [10, 25, 50],
|
|
||||||
rowClickable: false,
|
|
||||||
showActions: false,
|
|
||||||
emptyMessage: 'Aucune donnée',
|
|
||||||
loading: false,
|
|
||||||
rowClass: undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:page', value: number): void
|
|
||||||
(e: 'update:perPage', value: number): void
|
|
||||||
(e: 'row-click', item: T): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const perPageId = useId()
|
|
||||||
|
|
||||||
const currentPage = computed(() => props.page)
|
|
||||||
const currentPerPage = computed(() => props.perPage)
|
|
||||||
|
|
||||||
const isServerSide = computed(() => props.totalItems !== undefined)
|
|
||||||
const total = computed(() => props.totalItems ?? props.items.length)
|
|
||||||
|
|
||||||
const totalPages = computed(() =>
|
|
||||||
Math.max(1, Math.ceil(total.value / currentPerPage.value))
|
|
||||||
)
|
|
||||||
|
|
||||||
const paginatedItems = computed(() => {
|
|
||||||
if (isServerSide.value) return props.items
|
|
||||||
const start = (currentPage.value - 1) * currentPerPage.value
|
|
||||||
return props.items.slice(start, start + currentPerPage.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const gridCols = computed(() => {
|
|
||||||
const dataCols = props.columns.map(c => c.width ?? '1fr').join(' ')
|
|
||||||
return props.showActions ? `${dataCols} 60px` : dataCols
|
|
||||||
})
|
|
||||||
|
|
||||||
const dimRows = computed(() => props.loading && paginatedItems.value.length > 0)
|
|
||||||
|
|
||||||
const visiblePages = computed<(number | '...')[]>(() => {
|
|
||||||
const tp = totalPages.value
|
|
||||||
const cp = currentPage.value
|
|
||||||
|
|
||||||
if (tp <= 5) {
|
|
||||||
return Array.from({ length: tp }, (_, i) => i + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pages: (number | '...')[] = []
|
|
||||||
pages.push(1)
|
|
||||||
|
|
||||||
if (cp > 3) pages.push('...')
|
|
||||||
|
|
||||||
const start = Math.max(2, cp - 1)
|
|
||||||
const end = Math.min(tp - 1, cp + 1)
|
|
||||||
for (let i = start; i <= end; i++) pages.push(i)
|
|
||||||
|
|
||||||
if (cp < tp - 2) pages.push('...')
|
|
||||||
|
|
||||||
if (tp > 1) pages.push(tp)
|
|
||||||
|
|
||||||
return pages
|
|
||||||
})
|
|
||||||
|
|
||||||
const goToPage = (n: number) => {
|
|
||||||
if (n < 1 || n > totalPages.value || n === currentPage.value) return
|
|
||||||
emit('update:page', n)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPerPageChange = (value: string) => {
|
|
||||||
emit('update:perPage', Number(value))
|
|
||||||
emit('update:page', 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRowClick = (item: T) => {
|
|
||||||
if (!props.rowClickable) return
|
|
||||||
emit('row-click', item)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNestedValue = (obj: any, path: string): string => {
|
|
||||||
const value = path.split('.').reduce((acc, key) => acc?.[key], obj)
|
|
||||||
return value ?? '—'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<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="w-full min-w-0 border-b border-primary-700 justify-self-start text-primary-700 bg-transparent appearance-none"
|
|
||||||
:class="[
|
|
||||||
sizeClass,
|
|
||||||
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
|
|
||||||
size?: 'default' | 'compact'
|
|
||||||
wrapperClass?: string
|
|
||||||
labelClass?: string
|
|
||||||
inputClass?: string
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
disabled: false,
|
|
||||||
size: 'default',
|
|
||||||
wrapperClass: '',
|
|
||||||
labelClass: '',
|
|
||||||
inputClass: ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:modelValue', value: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const attrs = useAttrs()
|
|
||||||
const isEmpty = computed(() => !props.modelValue)
|
|
||||||
const sizeClass = computed(() =>
|
|
||||||
props.size === 'compact'
|
|
||||||
? 'text-sm h-8 font-normal normal-case tracking-normal'
|
|
||||||
: 'text-xl py-[6px] uppercase h-[34px]'
|
|
||||||
)
|
|
||||||
|
|
||||||
const onInput = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
emit('update:modelValue', target.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
<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"
|
|
||||||
v-maska="'##/##/####'"
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
:value="displayValue"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:disabled="disabled"
|
|
||||||
v-bind="attrs"
|
|
||||||
class="w-full min-w-0 border-b border-primary-700 bg-transparent"
|
|
||||||
:class="[
|
|
||||||
sizeClass,
|
|
||||||
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
|
||||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
|
||||||
inputClass
|
|
||||||
]"
|
|
||||||
@input="onInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { vMaska } from 'maska/vue'
|
|
||||||
import { computed, ref, useAttrs, watch } from 'vue'
|
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
id?: string
|
|
||||||
label?: string
|
|
||||||
modelValue: string | null | undefined
|
|
||||||
placeholder?: string
|
|
||||||
disabled?: boolean
|
|
||||||
size?: 'default' | 'compact'
|
|
||||||
wrapperClass?: string
|
|
||||||
labelClass?: string
|
|
||||||
inputClass?: string
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
placeholder: 'JJ/MM/AAAA',
|
|
||||||
disabled: false,
|
|
||||||
size: 'default',
|
|
||||||
wrapperClass: '',
|
|
||||||
labelClass: '',
|
|
||||||
inputClass: ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:modelValue', value: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const attrs = useAttrs()
|
|
||||||
|
|
||||||
const toDisplay = (iso: string | null | undefined): string => {
|
|
||||||
if (!iso) return ''
|
|
||||||
const parts = iso.split('-')
|
|
||||||
if (parts.length !== 3) return ''
|
|
||||||
const [year, month, day] = parts
|
|
||||||
if (year.length !== 4 || month.length !== 2 || day.length !== 2) return ''
|
|
||||||
return `${day}/${month}/${year}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const toIso = (display: string): string | null => {
|
|
||||||
const match = display.match(/^(\d{2})\/(\d{2})\/(\d{4})$/)
|
|
||||||
if (!match) return null
|
|
||||||
const [, day, month, year] = match
|
|
||||||
return `${year}-${month}-${day}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayValue = ref(toDisplay(props.modelValue))
|
|
||||||
|
|
||||||
watch(() => props.modelValue, (newIso) => {
|
|
||||||
const expected = toDisplay(newIso)
|
|
||||||
if (expected !== displayValue.value) {
|
|
||||||
displayValue.value = expected
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isEmpty = computed(() => !displayValue.value)
|
|
||||||
const sizeClass = computed(() =>
|
|
||||||
props.size === 'compact'
|
|
||||||
? 'text-sm h-8 font-normal normal-case tracking-normal'
|
|
||||||
: 'text-xl py-[6px]'
|
|
||||||
)
|
|
||||||
|
|
||||||
const onInput = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
displayValue.value = target.value
|
|
||||||
if (target.value === '') {
|
|
||||||
emit('update:modelValue', '')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const iso = toIso(target.value)
|
|
||||||
emit('update:modelValue', iso ?? '')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition duration-150 ease-out"
|
|
||||||
enter-from-class="opacity-0"
|
|
||||||
enter-to-class="opacity-100"
|
|
||||||
leave-active-class="transition duration-100 ease-in"
|
|
||||||
leave-from-class="opacity-100"
|
|
||||||
leave-to-class="opacity-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="modelValue"
|
|
||||||
class="fixed inset-0 z-40 flex items-center justify-center bg-black/50 px-4"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
@mousedown.self="closeOnBackdrop"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full rounded-md bg-white shadow-2xl"
|
|
||||||
:class="maxWidth"
|
|
||||||
@mousedown.stop
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between border-b border-slate-200 px-6 py-4">
|
|
||||||
<h2 class="text-xl font-bold uppercase text-primary-500">{{ title }}</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-slate-500 hover:text-primary-500 flex items-center"
|
|
||||||
aria-label="Fermer"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:close" size="24" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-6 py-5">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="$slots.footer"
|
|
||||||
class="border-t border-slate-200 px-6 py-4"
|
|
||||||
>
|
|
||||||
<slot name="footer" :close="close" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, onBeforeUnmount, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
title?: string
|
|
||||||
closeOnBackdropClick?: boolean
|
|
||||||
maxWidth?: string
|
|
||||||
}>(), {
|
|
||||||
title: '',
|
|
||||||
closeOnBackdropClick: true,
|
|
||||||
maxWidth: 'max-w-lg'
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: boolean): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const close = () => emit('update:modelValue', false)
|
|
||||||
|
|
||||||
const closeOnBackdrop = () => {
|
|
||||||
if (props.closeOnBackdropClick) close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape' && props.modelValue) close()
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.modelValue, (open) => {
|
|
||||||
if (typeof document === 'undefined') return
|
|
||||||
document.body.style.overflow = open ? 'hidden' : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
document.addEventListener('keydown', onKeydown)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
document.removeEventListener('keydown', onKeydown)
|
|
||||||
document.body.style.overflow = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// 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>
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<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="w-full min-w-0 border-b border-primary-700 justify-self-start text-primary-700 bg-transparent"
|
|
||||||
:class="[
|
|
||||||
sizeClass,
|
|
||||||
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
|
||||||
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
||||||
selectClass
|
|
||||||
]"
|
|
||||||
@change="onChange"
|
|
||||||
>
|
|
||||||
<option value="" 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
|
|
||||||
size?: 'default' | 'compact'
|
|
||||||
wrapperClass?: string
|
|
||||||
labelClass?: string
|
|
||||||
selectClass?: string
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
placeholder: 'Sélectionner',
|
|
||||||
disabled: false,
|
|
||||||
loading: false,
|
|
||||||
size: 'default',
|
|
||||||
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 sizeClass = computed(() =>
|
|
||||||
props.size === 'compact'
|
|
||||||
? 'text-sm h-8 font-normal normal-case tracking-normal'
|
|
||||||
: 'text-xl py-[6px]'
|
|
||||||
)
|
|
||||||
|
|
||||||
const onChange = (event: Event) => {
|
|
||||||
const target = event.target as HTMLSelectElement
|
|
||||||
emit('update:modelValue', target.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<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="w-full min-w-0 border-b border-primary-700 bg-transparent"
|
|
||||||
:class="[
|
|
||||||
sizeClass,
|
|
||||||
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
|
||||||
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
|
|
||||||
size?: 'default' | 'compact'
|
|
||||||
wrapperClass?: string
|
|
||||||
labelClass?: string
|
|
||||||
inputClass?: string
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
placeholder: '',
|
|
||||||
maxlength: undefined,
|
|
||||||
disabled: false,
|
|
||||||
size: 'default',
|
|
||||||
wrapperClass: '',
|
|
||||||
labelClass: '',
|
|
||||||
inputClass: ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'update:modelValue', value: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const attrs = useAttrs()
|
|
||||||
const isEmpty = computed(() => !props.modelValue)
|
|
||||||
const sizeClass = computed(() =>
|
|
||||||
props.size === 'compact'
|
|
||||||
? 'text-sm h-8 font-normal normal-case tracking-normal'
|
|
||||||
: 'text-xl py-[6px]'
|
|
||||||
)
|
|
||||||
|
|
||||||
const onInput = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
emit('update:modelValue', target.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label :for="inputId" class="font-bold uppercase text-xl text-primary-500">{{ label }}</label>
|
<label :for="inputId" class="font-bold uppercase text-xl mb-4">{{ label }}</label>
|
||||||
<div class="flex items-end gap-8">
|
|
||||||
<input
|
<input
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@@ -9,20 +8,19 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:maxlength="maxLength"
|
:maxlength="maxLength"
|
||||||
:placeholder="placeholderText"
|
:placeholder="placeholderText"
|
||||||
:required="required"
|
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
||||||
class="border-b border-primary-700 flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
|
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
<UiCheckbox
|
<label :for="checkboxId" class="mt-3 flex items-center gap-3 text-sm">
|
||||||
|
<input
|
||||||
:id="checkboxId"
|
:id="checkboxId"
|
||||||
:model-value="allowAny"
|
:checked="allowAny"
|
||||||
label="Autoriser un format libre"
|
type="checkbox"
|
||||||
wrapper-class="ml-auto"
|
class="h-4 w-4 accent-primary-500"
|
||||||
label-class="gap-3 whitespace-nowrap text-sm"
|
@change="toggleAllowAny"
|
||||||
input-class="h-4 w-4 accent-primary-500"
|
|
||||||
@update:modelValue="handleAllowAnyChange"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
Autoriser un format libre
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -33,14 +31,12 @@ type Props = {
|
|||||||
allowAny?: boolean
|
allowAny?: boolean
|
||||||
label?: string
|
label?: string
|
||||||
id?: string
|
id?: string
|
||||||
required?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
allowAny: false,
|
allowAny: false,
|
||||||
label: 'Immatriculation',
|
label: 'Immatriculation',
|
||||||
id: 'license-plate',
|
id: 'license-plate'
|
||||||
required: false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -82,7 +78,13 @@ const handleInput = (event: Event) => {
|
|||||||
emit('update:modelValue', target.value)
|
emit('update:modelValue', target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAllowAnyChange = (nextValue: boolean) => {
|
const toggleAllowAny = (event: Event) => {
|
||||||
|
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)
|
||||||
|
|||||||
20
frontend/components/ui/pdf-printer.vue
Normal file
20
frontend/components/ui/pdf-printer.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<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>
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<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,57 +0,0 @@
|
|||||||
<template>
|
|
||||||
<template v-if="!isLiotCarrier">
|
|
||||||
<div :class="wrapperClass">
|
|
||||||
<UiLicensePlateInput
|
|
||||||
v-model="form.licensePlate"
|
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-if="isLiotCarrier">
|
|
||||||
<UiSelect
|
|
||||||
:id="`${idPrefix}-vehicle`"
|
|
||||||
v-model="form.vehicleId"
|
|
||||||
label="Immatriculation"
|
|
||||||
:options="filteredVehicles.map((vehicle) => ({
|
|
||||||
value: String(vehicle.id),
|
|
||||||
label: vehicle.plate
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingVehicles"
|
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
|
||||||
:wrapper-class="wrapperClass"
|
|
||||||
/>
|
|
||||||
<UiSelect
|
|
||||||
:id="`${idPrefix}-driver`"
|
|
||||||
v-model="form.driverId"
|
|
||||||
label="Nom du chauffeur si LIOT"
|
|
||||||
:options="filteredDrivers.map((driver) => ({
|
|
||||||
value: String(driver.id),
|
|
||||||
label: driver.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingDrivers"
|
|
||||||
:wrapper-class="driverWrapperClass"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { DriverData } from '~/services/dto/driver-data'
|
|
||||||
import type { VehicleData } from '~/services/dto/vehicle-data'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
idPrefix: string
|
|
||||||
form: { licensePlate: string; vehicleId: string; driverId: string }
|
|
||||||
isLiotCarrier: boolean
|
|
||||||
allowAnyLicensePlate: boolean
|
|
||||||
filteredVehicles: VehicleData[]
|
|
||||||
filteredDrivers: DriverData[]
|
|
||||||
isLoadingVehicles: boolean
|
|
||||||
isLoadingDrivers: boolean
|
|
||||||
wrapperClass?: string
|
|
||||||
driverWrapperClass?: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
'update:allowAnyLicensePlate': [value: boolean]
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-10">
|
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
|
||||||
<h1 class="text-3xl font-bold uppercase text-primary-500">{{ title }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-[86px]">
|
|
||||||
<div class="mt-6 border border-slate-200 mb-16">
|
|
||||||
<div
|
|
||||||
class="grid gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
|
||||||
:style="{ gridTemplateColumns: gridCols }"
|
|
||||||
>
|
|
||||||
<div v-for="col in columns" :key="col.key">{{ col.label }}</div>
|
|
||||||
<div v-if="showActions">Actions</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="item in items"
|
|
||||||
:key="item.id"
|
|
||||||
class="grid gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
|
||||||
:style="{ gridTemplateColumns: gridCols }"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@click="goToItem(item.id)"
|
|
||||||
@keydown.enter="goToItem(item.id)"
|
|
||||||
>
|
|
||||||
<div v-for="col in columns" :key="col.key">
|
|
||||||
<slot :name="`cell-${col.key}`" :item="item">
|
|
||||||
{{ getNestedValue(item, col.key) }}
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
<div v-if="showActions" @click.stop>
|
|
||||||
<slot name="actions" :item="item" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Column {
|
|
||||||
key: string
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
title: string
|
|
||||||
columns: Column[]
|
|
||||||
items: any[]
|
|
||||||
routePrefix: string
|
|
||||||
showActions?: boolean
|
|
||||||
}>(), {
|
|
||||||
showActions: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const gridCols = computed(() => {
|
|
||||||
const dataCols = props.columns.map(() => '1fr').join(' ')
|
|
||||||
return props.showActions ? `${dataCols} 60px` : dataCols
|
|
||||||
})
|
|
||||||
|
|
||||||
const goToItem = (id: number) => {
|
|
||||||
router.push(`${props.routePrefix}/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNestedValue = (obj: any, path: string): string => {
|
|
||||||
const value = path.split('.').reduce((acc, key) => acc?.[key], obj)
|
|
||||||
return value || '—'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
|
||||||
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
|
||||||
<div
|
|
||||||
v-if="!displayWeight"
|
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
|
||||||
<UiLoadingDots />
|
|
||||||
</div>
|
|
||||||
<div v-else class="w-full">
|
|
||||||
<div
|
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
|
|
||||||
{{ displayWeight }} kg
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center mt-[54px]">
|
|
||||||
<UiButton
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
|
||||||
@click="fetchWeight"
|
|
||||||
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
|
||||||
<UiButton
|
|
||||||
v-if="displayWeight !== null && !showGenerateReceipt"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
|
||||||
@click="saveWeight"
|
|
||||||
>Valider la pesée</UiButton>
|
|
||||||
<UiButton
|
|
||||||
v-if="showGenerateReceipt"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
|
||||||
@click="printReceipt"
|
|
||||||
>Générer le bon</UiButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { toRef } from 'vue'
|
|
||||||
import { useWeighingStep } from '~/composables/steps/useWeighingStep'
|
|
||||||
import type { WeightData } from '~/services/dto/weight-data'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
mode: 'gross' | 'tare'
|
|
||||||
entityName: 'reception' | 'shipment'
|
|
||||||
apiResource: string
|
|
||||||
titleLabel: string
|
|
||||||
isFinal: boolean
|
|
||||||
entity: any
|
|
||||||
getWeightFromScale: () => Promise<WeightData>
|
|
||||||
updateEntity: (id: number, payload: any) => Promise<any>
|
|
||||||
loadEntity: (id: number) => Promise<any>
|
|
||||||
clearEntity: () => void
|
|
||||||
buildReceiptFilename: (entity: any) => string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const entityRef = toRef(props, 'entity')
|
|
||||||
|
|
||||||
const {
|
|
||||||
displayWeight,
|
|
||||||
title,
|
|
||||||
fetchWeight,
|
|
||||||
saveWeight,
|
|
||||||
saveWeightDraft,
|
|
||||||
showGenerateReceipt,
|
|
||||||
printReceipt
|
|
||||||
} = useWeighingStep({
|
|
||||||
mode: props.mode,
|
|
||||||
entity: entityRef,
|
|
||||||
entityName: props.entityName,
|
|
||||||
apiResource: props.apiResource,
|
|
||||||
titleLabel: props.titleLabel,
|
|
||||||
isFinal: props.isFinal,
|
|
||||||
getWeightFromScale: props.getWeightFromScale,
|
|
||||||
updateEntity: props.updateEntity,
|
|
||||||
loadEntity: props.loadEntity,
|
|
||||||
clearEntity: props.clearEntity,
|
|
||||||
buildReceiptFilename: props.buildReceiptFilename
|
|
||||||
})
|
|
||||||
|
|
||||||
defineExpose({ saveWeightDraft })
|
|
||||||
</script>
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import type { Ref } from 'vue'
|
|
||||||
import { useWeighing } from '~/composables/useWeighing'
|
|
||||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
|
||||||
import type { WeightData } from '~/services/dto/weight-data'
|
|
||||||
|
|
||||||
interface UseWeighingStepOptions {
|
|
||||||
mode: 'gross' | 'tare'
|
|
||||||
entity: Ref<any>
|
|
||||||
entityName: 'reception' | 'shipment'
|
|
||||||
apiResource: string
|
|
||||||
titleLabel: string
|
|
||||||
isFinal: boolean
|
|
||||||
getWeightFromScale: () => Promise<WeightData>
|
|
||||||
updateEntity: (id: number, payload: any) => Promise<any>
|
|
||||||
loadEntity: (id: number) => Promise<any>
|
|
||||||
clearEntity: () => void
|
|
||||||
buildReceiptFilename: (entity: any) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWeighingStep = (options: UseWeighingStepOptions) => {
|
|
||||||
const router = useRouter()
|
|
||||||
const { printPdf } = usePdfPrinter()
|
|
||||||
|
|
||||||
const {
|
|
||||||
weightData,
|
|
||||||
currentWeightEntry,
|
|
||||||
displayWeight,
|
|
||||||
displayDsd,
|
|
||||||
title,
|
|
||||||
showLoadingBox,
|
|
||||||
fetchWeight,
|
|
||||||
saveWeight,
|
|
||||||
saveWeightDraft
|
|
||||||
} = useWeighing({
|
|
||||||
mode: options.mode,
|
|
||||||
entity: options.entity,
|
|
||||||
entityName: options.entityName,
|
|
||||||
apiResource: options.apiResource,
|
|
||||||
titleLabel: options.titleLabel,
|
|
||||||
isFinal: options.isFinal,
|
|
||||||
getWeightFromScale: options.getWeightFromScale,
|
|
||||||
updateEntity: options.updateEntity,
|
|
||||||
loadEntity: options.loadEntity
|
|
||||||
})
|
|
||||||
|
|
||||||
const showGenerateReceipt = computed(
|
|
||||||
() => options.isFinal && displayWeight.value !== null
|
|
||||||
)
|
|
||||||
|
|
||||||
const printReceipt = async () => {
|
|
||||||
if (!import.meta.client || !options.entity.value) return
|
|
||||||
|
|
||||||
await saveWeight()
|
|
||||||
const entity = options.entity.value
|
|
||||||
const filename = options.buildReceiptFilename(entity)
|
|
||||||
await printPdf(`/${options.apiResource}/${entity.id}/receipt`, filename)
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
|
||||||
|
|
||||||
const result = await options.updateEntity(entity.id, { isValid: true })
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
options.clearEntity()
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
weightData,
|
|
||||||
currentWeightEntry,
|
|
||||||
displayWeight,
|
|
||||||
displayDsd,
|
|
||||||
title,
|
|
||||||
showLoadingBox,
|
|
||||||
fetchWeight,
|
|
||||||
saveWeight,
|
|
||||||
saveWeightDraft,
|
|
||||||
showGenerateReceipt,
|
|
||||||
printReceipt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import type { Ref } from 'vue'
|
|
||||||
import type { AddressData } from '~/services/dto/address-data'
|
|
||||||
|
|
||||||
interface AddressOwner {
|
|
||||||
id: number
|
|
||||||
addresses?: AddressData[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAddressSync = (
|
|
||||||
form: { addressId: string },
|
|
||||||
ownerId: Ref<string>,
|
|
||||||
owners: Ref<AddressOwner[]>
|
|
||||||
) => {
|
|
||||||
const ownerAddresses = computed<AddressData[]>(() => {
|
|
||||||
const id = Number(ownerId.value)
|
|
||||||
if (!Number.isFinite(id)) return []
|
|
||||||
return owners.value.find((owner) => owner.id === id)?.addresses ?? []
|
|
||||||
})
|
|
||||||
|
|
||||||
const addressOptions = computed(() =>
|
|
||||||
ownerAddresses.value.map((address) => ({
|
|
||||||
value: String(address.id),
|
|
||||||
label: address.fullAddress
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [ownerId.value, form.addressId, owners.value],
|
|
||||||
() => {
|
|
||||||
if (!ownerId.value) {
|
|
||||||
form.addressId = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.addressId && ownerAddresses.value.length === 1) {
|
|
||||||
form.addressId = String(ownerAddresses.value[0].id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.addressId) return
|
|
||||||
const matches = ownerAddresses.value.some(
|
|
||||||
(address) => String(address.id) === form.addressId
|
|
||||||
)
|
|
||||||
if (!matches) {
|
|
||||||
if (ownerAddresses.value.length === 1) {
|
|
||||||
form.addressId = String(ownerAddresses.value[0].id)
|
|
||||||
} else {
|
|
||||||
form.addressId = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
return { ownerAddresses, addressOptions }
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ export const useApi = (): ApiClient => {
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const auth = useAuthStore()
|
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
|
||||||
@@ -96,23 +95,7 @@ export const useApi = (): ApiClient => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onResponseError({ response, error, options }) {
|
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
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
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,113 +0,0 @@
|
|||||||
import { ref, onUnmounted } from 'vue'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
BarcodeDetector: new (options?: { formats: string[] }) => {
|
|
||||||
detect(source: HTMLVideoElement | ImageBitmapSource): Promise<{ rawValue: string }[]>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const BarcodeDetector: Window['BarcodeDetector'] | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBarcodeScanner(onDetected: (code: string) => void) {
|
|
||||||
const isSupported = ref('BarcodeDetector' in globalThis)
|
|
||||||
const isScanning = ref(false)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
|
|
||||||
let detector: InstanceType<Window['BarcodeDetector']> | null = null
|
|
||||||
let stream: MediaStream | null = null
|
|
||||||
let animationFrameId: number | null = null
|
|
||||||
let lastDetectedCode = ''
|
|
||||||
let lastDetectedTime = 0
|
|
||||||
|
|
||||||
const COOLDOWN_MS = 2000
|
|
||||||
|
|
||||||
async function start(videoElement: HTMLVideoElement) {
|
|
||||||
if (!isSupported.value) {
|
|
||||||
error.value = 'BarcodeDetector non supporté. Utilisez Chrome sur Android.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
detector = new BarcodeDetector({ formats: ['code_39', 'code_128'] })
|
|
||||||
|
|
||||||
stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: {
|
|
||||||
facingMode: 'environment',
|
|
||||||
width: { ideal: 1280 },
|
|
||||||
height: { ideal: 720 }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
videoElement.srcObject = stream
|
|
||||||
await videoElement.play()
|
|
||||||
isScanning.value = true
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
scanLoop(videoElement)
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e instanceof Error ? e.message : 'Erreur lors du démarrage de la caméra'
|
|
||||||
isScanning.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scanLoop(videoElement: HTMLVideoElement) {
|
|
||||||
if (!isScanning.value || !detector) return
|
|
||||||
|
|
||||||
animationFrameId = requestAnimationFrame(async () => {
|
|
||||||
try {
|
|
||||||
if (videoElement.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
|
|
||||||
const barcodes = await detector!.detect(videoElement)
|
|
||||||
|
|
||||||
if (barcodes.length > 0) {
|
|
||||||
const code = barcodes[0].rawValue.slice(4)
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
if (code !== lastDetectedCode || now - lastDetectedTime > COOLDOWN_MS) {
|
|
||||||
lastDetectedCode = code
|
|
||||||
lastDetectedTime = now
|
|
||||||
|
|
||||||
if (navigator.vibrate) {
|
|
||||||
navigator.vibrate(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
onDetected(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Detection error on single frame, continue
|
|
||||||
}
|
|
||||||
|
|
||||||
scanLoop(videoElement)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function stop() {
|
|
||||||
isScanning.value = false
|
|
||||||
|
|
||||||
if (animationFrameId !== null) {
|
|
||||||
cancelAnimationFrame(animationFrameId)
|
|
||||||
animationFrameId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stream) {
|
|
||||||
stream.getTracks().forEach(track => track.stop())
|
|
||||||
stream = null
|
|
||||||
}
|
|
||||||
|
|
||||||
detector = null
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
isSupported,
|
|
||||||
isScanning,
|
|
||||||
error,
|
|
||||||
start,
|
|
||||||
stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { computed } from 'vue'
|
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
export interface BovineColumn {
|
|
||||||
key: string
|
|
||||||
label: string
|
|
||||||
width?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseBovineColumnsOptions {
|
|
||||||
/**
|
|
||||||
* 'inventory' (par défaut) : colonnes complètes incluant Bâtiment + Case.
|
|
||||||
* 'case' : pas de Bâtiment ni Case (déjà dans le titre de la page),
|
|
||||||
* largeurs élargies pour combler l'espace.
|
|
||||||
*/
|
|
||||||
variant?: 'inventory' | 'case'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Définition partagée des colonnes des tableaux bovins (inventory + case).
|
|
||||||
* 4 variants : avec/sans colonnes prix × inventory/case.
|
|
||||||
*
|
|
||||||
* Les colonnes Prix/kg et Prix total sont visibles pour les rôles BUREAU
|
|
||||||
* et ADMIN (BUREAU hérite ses droits price-visibility, ADMIN hérite de BUREAU).
|
|
||||||
*/
|
|
||||||
export const useBovineColumns = (options: UseBovineColumnsOptions = {}) => {
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
const withPricesInventory: BovineColumn[] = [
|
|
||||||
{ key: 'nationalNumber', label: 'N° National', width: '80px' },
|
|
||||||
{ key: 'workNumber', label: 'N° Travail', width: '60px' },
|
|
||||||
{ key: 'sex', label: 'Sexe', width: '70px' },
|
|
||||||
{ key: 'birthDate', label: 'Né le', width: '72px' },
|
|
||||||
{ key: 'age', label: 'Age', width: '110px' },
|
|
||||||
{ key: 'bovineType.label', label: 'Race', width: '90px' },
|
|
||||||
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '1fr' },
|
|
||||||
{ key: 'buildingCase.caseNumber', label: 'Case', width: '42px' },
|
|
||||||
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' },
|
|
||||||
{ key: 'pricePerKg', label: 'Prix/kg', width: '65px' },
|
|
||||||
{ key: 'finalPrice', label: 'Prix total', width: '80px' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const withoutPricesInventory: BovineColumn[] = [
|
|
||||||
{ key: 'nationalNumber', label: 'N° National', width: '80px' },
|
|
||||||
{ key: 'workNumber', label: 'N° Travail', width: '60px' },
|
|
||||||
{ key: 'sex', label: 'Sexe', width: '70px' },
|
|
||||||
{ key: 'birthDate', label: 'Né le', width: '72px' },
|
|
||||||
{ key: 'age', label: 'Age', width: '110px' },
|
|
||||||
{ key: 'bovineType.label', label: 'Race', width: '1fr' },
|
|
||||||
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '120px' },
|
|
||||||
{ key: 'buildingCase.caseNumber', label: 'Case', width: '42px' },
|
|
||||||
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const withPricesCase: BovineColumn[] = [
|
|
||||||
{ key: 'nationalNumber', label: 'N° National', width: '110px' },
|
|
||||||
{ key: 'workNumber', label: 'N° Travail', width: '85px' },
|
|
||||||
{ key: 'sex', label: 'Sexe', width: '90px' },
|
|
||||||
{ key: 'birthDate', label: 'Né le', width: '100px' },
|
|
||||||
{ key: 'age', label: 'Age', width: '90px' },
|
|
||||||
{ key: 'bovineType.label', label: 'Race', width: '1fr' },
|
|
||||||
{ key: 'arrivalDate', label: 'Entrée le', width: '110px' },
|
|
||||||
{ key: 'pricePerKg', label: 'Prix/kg', width: '85px' },
|
|
||||||
{ key: 'finalPrice', label: 'Prix total', width: '105px' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const withoutPricesCase: BovineColumn[] = [
|
|
||||||
{ key: 'nationalNumber', label: 'N° National', width: '130px' },
|
|
||||||
{ key: 'workNumber', label: 'N° Travail', width: '100px' },
|
|
||||||
{ key: 'sex', label: 'Sexe', width: '110px' },
|
|
||||||
{ key: 'birthDate', label: 'Né le', width: '140px' },
|
|
||||||
{ key: 'age', label: 'Age', width: '130px' },
|
|
||||||
{ key: 'bovineType.label', label: 'Race', width: '1fr' },
|
|
||||||
{ key: 'arrivalDate', label: 'Entrée le', width: '170px' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const columns = computed<BovineColumn[]>(() => {
|
|
||||||
const isCase = options.variant === 'case'
|
|
||||||
const seePrice = auth.isBureau
|
|
||||||
|
|
||||||
if (isCase) {
|
|
||||||
return seePrice ? withPricesCase : withoutPricesCase
|
|
||||||
}
|
|
||||||
return seePrice ? withPricesInventory : withoutPricesInventory
|
|
||||||
})
|
|
||||||
|
|
||||||
return { columns }
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { ref, watch } from 'vue'
|
|
||||||
import { useApi } from '~/composables/useApi'
|
|
||||||
|
|
||||||
type FilterValue = string | number | boolean | null
|
|
||||||
|
|
||||||
export interface UseDataTableServerStateOptions {
|
|
||||||
initialPerPage?: number
|
|
||||||
debounceMs?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDataTableServerState<T = Record<string, unknown>>(
|
|
||||||
endpoint: string,
|
|
||||||
initialFilters: Record<string, FilterValue> = {},
|
|
||||||
options: UseDataTableServerStateOptions = {}
|
|
||||||
) {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const debounceMs = options.debounceMs ?? 300
|
|
||||||
const initialPerPage = options.initialPerPage ?? 10
|
|
||||||
|
|
||||||
const items = ref<T[]>([]) as { value: T[] }
|
|
||||||
const totalItems = ref(0)
|
|
||||||
const page = ref(1)
|
|
||||||
const perPage = ref(initialPerPage)
|
|
||||||
const filters = ref<Record<string, FilterValue>>({ ...initialFilters })
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
let requestToken = 0
|
|
||||||
|
|
||||||
const buildQueryParams = (): Record<string, string | number | boolean> => {
|
|
||||||
const params: Record<string, string | number | boolean> = {
|
|
||||||
page: page.value,
|
|
||||||
itemsPerPage: perPage.value
|
|
||||||
}
|
|
||||||
for (const [key, value] of Object.entries(filters.value)) {
|
|
||||||
if (value === '' || value === null) continue
|
|
||||||
params[key] = value as string | number | boolean
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchItems = async (): Promise<void> => {
|
|
||||||
const currentToken = ++requestToken
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await api.get<{ member: T[]; totalItems: number }>(
|
|
||||||
endpoint,
|
|
||||||
buildQueryParams(),
|
|
||||||
{
|
|
||||||
toast: false,
|
|
||||||
headers: { Accept: 'application/ld+json' }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (currentToken !== requestToken) return
|
|
||||||
items.value = data?.member ?? []
|
|
||||||
totalItems.value = data?.totalItems ?? 0
|
|
||||||
} finally {
|
|
||||||
if (currentToken === requestToken) {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = (): void => {
|
|
||||||
if (debounceTimer) {
|
|
||||||
clearTimeout(debounceTimer)
|
|
||||||
debounceTimer = null
|
|
||||||
}
|
|
||||||
void fetchItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleReload = (): void => {
|
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
|
||||||
debounceTimer = setTimeout(() => {
|
|
||||||
debounceTimer = null
|
|
||||||
void fetchItems()
|
|
||||||
}, debounceMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([page, perPage], () => {
|
|
||||||
reload()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(filters, () => {
|
|
||||||
if (page.value !== 1) {
|
|
||||||
page.value = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scheduleReload()
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
totalItems,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
filters,
|
|
||||||
loading,
|
|
||||||
reload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import type { UserData } from '~/services/dto/user-data'
|
|
||||||
import type { TruckData } from '~/services/dto/truck-data'
|
|
||||||
import type { CarrierData } from '~/services/dto/carrier-data'
|
|
||||||
import { getUsers } from '~/services/auth'
|
|
||||||
import { getTruckList } from '~/services/truck'
|
|
||||||
import { getCarrierList } from '~/services/carrier'
|
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
export const useFormDataLoading = (form: { userId: string }) => {
|
|
||||||
const users = ref<UserData[]>([])
|
|
||||||
const trucks = ref<TruckData[]>([])
|
|
||||||
const carriers = ref<CarrierData[]>([])
|
|
||||||
const isLoadingUsers = ref(false)
|
|
||||||
const isLoadingTrucks = ref(false)
|
|
||||||
const isLoadingCarriers = ref(false)
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
const loadUsers = async () => {
|
|
||||||
isLoadingUsers.value = true
|
|
||||||
try {
|
|
||||||
users.value = await getUsers()
|
|
||||||
} finally {
|
|
||||||
isLoadingUsers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadTrucks = async () => {
|
|
||||||
isLoadingTrucks.value = true
|
|
||||||
try {
|
|
||||||
trucks.value = await getTruckList()
|
|
||||||
} finally {
|
|
||||||
isLoadingTrucks.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadCarriers = async () => {
|
|
||||||
isLoadingCarriers.value = true
|
|
||||||
try {
|
|
||||||
carriers.value = await getCarrierList()
|
|
||||||
} finally {
|
|
||||||
isLoadingCarriers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setDefaultUser = () => {
|
|
||||||
if (form.userId) return
|
|
||||||
if (authStore.user?.id) {
|
|
||||||
form.userId = String(authStore.user.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadCommonData = async () => {
|
|
||||||
await loadUsers()
|
|
||||||
await loadTrucks()
|
|
||||||
await loadCarriers()
|
|
||||||
await authStore.ensureSession()
|
|
||||||
setDefaultUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
users,
|
|
||||||
trucks,
|
|
||||||
carriers,
|
|
||||||
isLoadingUsers,
|
|
||||||
isLoadingTrucks,
|
|
||||||
isLoadingCarriers,
|
|
||||||
loadUsers,
|
|
||||||
loadTrucks,
|
|
||||||
loadCarriers,
|
|
||||||
setDefaultUser,
|
|
||||||
loadCommonData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import type { Ref } from 'vue'
|
|
||||||
import type { CarrierData } from '~/services/dto/carrier-data'
|
|
||||||
import type { DriverData } from '~/services/dto/driver-data'
|
|
||||||
import type { VehicleData } from '~/services/dto/vehicle-data'
|
|
||||||
import { getDriverList } from '~/services/driver'
|
|
||||||
import { getVehicleList } from '~/services/vehicle'
|
|
||||||
import { SUPPLIER_CODE } from '~/utils/constants'
|
|
||||||
|
|
||||||
interface LiotForm {
|
|
||||||
carrierId: string
|
|
||||||
truckId: string
|
|
||||||
driverId: string
|
|
||||||
vehicleId: string
|
|
||||||
licensePlate: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useLiotHandling = (
|
|
||||||
form: LiotForm,
|
|
||||||
carriers: Ref<CarrierData[]>,
|
|
||||||
isHydrating: Ref<boolean>
|
|
||||||
) => {
|
|
||||||
const drivers = ref<DriverData[]>([])
|
|
||||||
const vehicles = ref<VehicleData[]>([])
|
|
||||||
const isLoadingDrivers = ref(false)
|
|
||||||
const isLoadingVehicles = ref(false)
|
|
||||||
const allowAnyLicensePlate = ref(false)
|
|
||||||
|
|
||||||
const selectedCarrier = computed(() =>
|
|
||||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
|
||||||
)
|
|
||||||
|
|
||||||
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
|
||||||
|
|
||||||
const filteredDrivers = computed<DriverData[]>(() => {
|
|
||||||
if (!form.carrierId) return []
|
|
||||||
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredVehicles = computed<VehicleData[]>(() => {
|
|
||||||
if (!form.carrierId) return []
|
|
||||||
return vehicles.value.filter(
|
|
||||||
(vehicle) =>
|
|
||||||
String(vehicle.carrier?.id) === form.carrierId &&
|
|
||||||
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadDrivers = async () => {
|
|
||||||
isLoadingDrivers.value = true
|
|
||||||
try {
|
|
||||||
drivers.value = await getDriverList()
|
|
||||||
} finally {
|
|
||||||
isLoadingDrivers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadVehicles = async () => {
|
|
||||||
isLoadingVehicles.value = true
|
|
||||||
try {
|
|
||||||
vehicles.value = await getVehicleList()
|
|
||||||
} finally {
|
|
||||||
isLoadingVehicles.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-select driver/vehicle when carrier changes
|
|
||||||
watch(
|
|
||||||
() => form.carrierId,
|
|
||||||
() => {
|
|
||||||
if (isHydrating.value) return
|
|
||||||
if (!form.carrierId) {
|
|
||||||
form.driverId = ''
|
|
||||||
form.vehicleId = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!isLiotCarrier.value) {
|
|
||||||
form.driverId = ''
|
|
||||||
form.vehicleId = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (filteredDrivers.value.length === 1) {
|
|
||||||
form.driverId = String(filteredDrivers.value[0].id)
|
|
||||||
}
|
|
||||||
if (filteredVehicles.value.length === 1) {
|
|
||||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate/auto-select vehicle when truck/carrier changes
|
|
||||||
watch(
|
|
||||||
() => [form.truckId, form.carrierId, vehicles.value],
|
|
||||||
() => {
|
|
||||||
if (!isLiotCarrier.value) return
|
|
||||||
if (filteredVehicles.value.length === 1) {
|
|
||||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.vehicleId) return
|
|
||||||
const matches = filteredVehicles.value.some(
|
|
||||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
|
||||||
)
|
|
||||||
if (!matches) {
|
|
||||||
form.vehicleId = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sync license plate from selected vehicle
|
|
||||||
watch(
|
|
||||||
() => [form.vehicleId, form.carrierId, vehicles.value],
|
|
||||||
() => {
|
|
||||||
if (!isLiotCarrier.value) return
|
|
||||||
if (isHydrating.value) return
|
|
||||||
const selected = filteredVehicles.value.find(
|
|
||||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
|
||||||
)
|
|
||||||
if (selected) {
|
|
||||||
form.licensePlate = selected.plate
|
|
||||||
allowAnyLicensePlate.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Auto-select vehicle from license plate
|
|
||||||
watch(
|
|
||||||
() => [form.licensePlate, form.carrierId, vehicles.value],
|
|
||||||
() => {
|
|
||||||
if (!isLiotCarrier.value || form.vehicleId) return
|
|
||||||
const match = filteredVehicles.value.find(
|
|
||||||
(vehicle) => vehicle.plate === form.licensePlate
|
|
||||||
)
|
|
||||||
if (match) {
|
|
||||||
form.vehicleId = String(match.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
drivers,
|
|
||||||
vehicles,
|
|
||||||
isLoadingDrivers,
|
|
||||||
isLoadingVehicles,
|
|
||||||
allowAnyLicensePlate,
|
|
||||||
isLiotCarrier,
|
|
||||||
filteredDrivers,
|
|
||||||
filteredVehicles,
|
|
||||||
loadDrivers,
|
|
||||||
loadVehicles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,37 @@
|
|||||||
|
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, filename = 'document.pdf'): Promise<void> => {
|
const printPdf = async (url: string, frameRef: PrintFrameRef): Promise<void> => {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = frameRef.value
|
||||||
|
if (!frame) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// On charge le PDF en blob pour rester en same-origin dans l'iframe.
|
||||||
const blob = await api.getBlob(url)
|
const blob = await api.getBlob(url)
|
||||||
|
const blobUrl = URL.createObjectURL(blob)
|
||||||
|
|
||||||
const pdfBlob = blob.type === 'application/pdf'
|
const tryPrint = () => {
|
||||||
? blob
|
frame.contentWindow?.focus()
|
||||||
: new Blob([blob], { type: 'application/pdf' })
|
frame.contentWindow?.print()
|
||||||
|
}
|
||||||
|
|
||||||
const blobUrl = URL.createObjectURL(pdfBlob)
|
frame.onload = () => {
|
||||||
|
tryPrint()
|
||||||
const a = document.createElement('a')
|
// On libere l'URL blob apres l'impression.
|
||||||
a.href = blobUrl
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 2000)
|
||||||
a.download = filename
|
}
|
||||||
a.style.display = 'none'
|
frame.src = blobUrl
|
||||||
document.body.appendChild(a)
|
setTimeout(tryPrint, 1200)
|
||||||
a.click()
|
|
||||||
a.remove()
|
|
||||||
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
|
|
||||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,66 +1,60 @@
|
|||||||
import { computed, ref } from 'vue'
|
import type {Ref} from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
import type { WeightEntryData } from '~/services/dto/reception-data'
|
import type {ReceptionData, WeightEntryData} from '~/services/dto/reception-data'
|
||||||
import type { WeightData } from '~/services/dto/weight-data'
|
import type {WeightData} from '~/services/dto/weight-data'
|
||||||
import { createWeight, updateWeight } from '~/services/weight'
|
import {getWeight} from '~/services/reception'
|
||||||
|
import {createWeight, updateWeight} from '~/services/weight'
|
||||||
|
|
||||||
export type WeighingMode = 'gross' | 'tare'
|
export type WeighingMode = 'gross' | 'tare'
|
||||||
|
|
||||||
export interface UseWeighingOptions {
|
type UseWeighingOptions = {
|
||||||
mode: WeighingMode
|
mode: WeighingMode
|
||||||
entity: Ref<{ id: number; currentStep: number; isValid: boolean; weights?: WeightEntryData[] | null } | null>
|
reception: Ref<ReceptionData | null>
|
||||||
entityName: 'reception' | 'shipment'
|
updateReception: (id: number, payload: Partial<ReceptionData>) => Promise<ReceptionData | null>
|
||||||
apiResource: string
|
loadReception?: (id: number) => Promise<ReceptionData | null>
|
||||||
titleLabel: string
|
|
||||||
isFinal?: boolean
|
|
||||||
getWeightFromScale: () => Promise<WeightData>
|
|
||||||
updateEntity: (id: number, payload: any) => Promise<any>
|
|
||||||
loadEntity?: (id: number) => Promise<any>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWeighing = ({
|
export const useWeighing = ({
|
||||||
mode,
|
mode,
|
||||||
entity,
|
reception,
|
||||||
entityName,
|
updateReception,
|
||||||
apiResource,
|
loadReception
|
||||||
titleLabel,
|
|
||||||
isFinal = false,
|
|
||||||
getWeightFromScale,
|
|
||||||
updateEntity,
|
|
||||||
loadEntity
|
|
||||||
}: UseWeighingOptions) => {
|
}: UseWeighingOptions) => {
|
||||||
const weightData = ref<WeightData | null>(null)
|
const weightData = ref<WeightData | null>(null)
|
||||||
const isFetching = ref(false)
|
const isFetching = ref(false)
|
||||||
|
|
||||||
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
||||||
const weights = entity.value?.weights ?? []
|
const weights = reception.value?.weights ?? []
|
||||||
return weights.find((entry) => entry.type === mode) ?? null
|
return weights.find((entry) => entry.type === mode) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
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(() => titleLabel)
|
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
||||||
const showLoadingBox = computed(() => isFetching.value)
|
const showLoadingBox = computed(
|
||||||
|
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
||||||
|
)
|
||||||
|
|
||||||
const fetchWeight = async () => {
|
const fetchWeight = async () => {
|
||||||
isFetching.value = true
|
isFetching.value = true
|
||||||
weightData.value = await getWeightFromScale().finally(() => {
|
weightData.value = await getWeight().finally(() => {
|
||||||
isFetching.value = false
|
isFetching.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveWeight = async () => {
|
const saveWeight = async () => {
|
||||||
if (!entity.value) return
|
if (!reception.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const existingEntry = currentWeightEntry.value
|
const existingEntry = currentWeightEntry.value
|
||||||
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
||||||
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
||||||
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||||
|
|
||||||
if (baseWeight === null) return
|
if (baseWeight === null) {
|
||||||
|
return
|
||||||
const relationPayload: Record<string, string> = {}
|
}
|
||||||
relationPayload[entityName] = `/api/${apiResource}/${entity.value.id}`
|
|
||||||
|
|
||||||
if (existingEntry?.id) {
|
if (existingEntry?.id) {
|
||||||
await updateWeight(existingEntry.id, {
|
await updateWeight(existingEntry.id, {
|
||||||
@@ -71,7 +65,7 @@ export const useWeighing = ({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await createWeight({
|
await createWeight({
|
||||||
...relationPayload,
|
reception: `/receptions/${reception.value.id}`,
|
||||||
type: mode,
|
type: mode,
|
||||||
dsd: baseDsd,
|
dsd: baseDsd,
|
||||||
weight: baseWeight,
|
weight: baseWeight,
|
||||||
@@ -79,48 +73,16 @@ export const useWeighing = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = isFinal
|
const nextStep = mode === 'tare'
|
||||||
? entity.value.currentStep
|
? reception.value.currentStep
|
||||||
: entity.value.currentStep + 1
|
: reception.value.currentStep + 1
|
||||||
await updateEntity(entity.value.id, {
|
await updateReception(reception.value.id, {
|
||||||
currentStep: nextStep,
|
currentStep: nextStep,
|
||||||
isValid: entity.value.isValid
|
isValid: reception.value.isValid
|
||||||
})
|
})
|
||||||
|
|
||||||
if (loadEntity) {
|
if (loadReception) {
|
||||||
await loadEntity(entity.value.id)
|
await loadReception(reception.value.id)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveWeightDraft = async () => {
|
|
||||||
if (!entity.value) return
|
|
||||||
if (!weightData.value && !currentWeightEntry.value) return
|
|
||||||
|
|
||||||
const existingEntry = currentWeightEntry.value
|
|
||||||
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
|
||||||
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
|
||||||
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
|
||||||
|
|
||||||
if (baseWeight === null) return
|
|
||||||
|
|
||||||
const relationPayload: Record<string, string> = {}
|
|
||||||
relationPayload[entityName] = `/api/${apiResource}/${entity.value.id}`
|
|
||||||
|
|
||||||
if (existingEntry?.id) {
|
|
||||||
await updateWeight(existingEntry.id, {
|
|
||||||
type: mode,
|
|
||||||
dsd: baseDsd,
|
|
||||||
weight: baseWeight,
|
|
||||||
weighedAt: baseWeighedAt
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await createWeight({
|
|
||||||
...relationPayload,
|
|
||||||
type: mode,
|
|
||||||
dsd: baseDsd,
|
|
||||||
weight: baseWeight,
|
|
||||||
weighedAt: baseWeighedAt
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,34 +94,6 @@ export const useWeighing = ({
|
|||||||
title,
|
title,
|
||||||
showLoadingBox,
|
showLoadingBox,
|
||||||
fetchWeight,
|
fetchWeight,
|
||||||
saveWeight,
|
saveWeight
|
||||||
saveWeightDraft
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward-compatible aliases
|
|
||||||
export const useWeighingShipment = ({
|
|
||||||
modeShipment,
|
|
||||||
shipment,
|
|
||||||
updateShipment,
|
|
||||||
loadShipment
|
|
||||||
}: {
|
|
||||||
modeShipment: WeighingMode
|
|
||||||
shipment: Ref<any>
|
|
||||||
updateShipment: (id: number, payload: any) => Promise<any>
|
|
||||||
loadShipment?: (id: number) => Promise<any>
|
|
||||||
}) => {
|
|
||||||
return useWeighing({
|
|
||||||
mode: modeShipment,
|
|
||||||
entity: shipment,
|
|
||||||
entityName: 'shipment',
|
|
||||||
apiResource: 'shipments',
|
|
||||||
titleLabel: modeShipment === 'gross' ? 'Pesée à plein' : 'Pesée à vide',
|
|
||||||
getWeightFromScale: async () => {
|
|
||||||
const { getWeightShipment } = await import('~/services/shipment')
|
|
||||||
return getWeightShipment()
|
|
||||||
},
|
|
||||||
updateEntity: updateShipment,
|
|
||||||
loadEntity: loadShipment
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import type { Ref } from 'vue'
|
|
||||||
import type { WorkflowConfig } from '~/types/workflow'
|
|
||||||
|
|
||||||
interface WorkflowStore {
|
|
||||||
current: any
|
|
||||||
isLoading: boolean
|
|
||||||
clearCurrent: () => void
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWorkflowSteps = (config: WorkflowConfig, store: WorkflowStore) => {
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const stepLabels = config.steps.map(s => s.label)
|
|
||||||
|
|
||||||
const currentStep = computed(() => store.current?.currentStep ?? 0)
|
|
||||||
const entity = computed(() => store.current)
|
|
||||||
|
|
||||||
const loadMethod = `load${config.entityName.charAt(0).toUpperCase() + config.entityName.slice(1)}`
|
|
||||||
const updateMethod = `update${config.entityName.charAt(0).toUpperCase() + config.entityName.slice(1)}`
|
|
||||||
|
|
||||||
const resolveId = (param: unknown) => {
|
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
|
||||||
if (!idStr) return null
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
watch(
|
|
||||||
() => route.params.id,
|
|
||||||
async (param) => {
|
|
||||||
const id = resolveId(param)
|
|
||||||
if (id === null) {
|
|
||||||
store.clearCurrent()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await store[loadMethod](id)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveAndHold = async () => {
|
|
||||||
if (!store.current) {
|
|
||||||
await router.push('/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const datePayload: Record<string, any> = {}
|
|
||||||
const rawDate = store.current[config.dateField]
|
|
||||||
datePayload[config.dateField] = rawDate ? rawDate.slice(0, 10) : rawDate
|
|
||||||
await store[updateMethod](store.current.id, {
|
|
||||||
currentStep: store.current.currentStep,
|
|
||||||
licensePlate: store.current.licensePlate,
|
|
||||||
...datePayload
|
|
||||||
})
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStepSelect = async (step: number) => {
|
|
||||||
if (!store.current) return
|
|
||||||
if (step === store.current.currentStep) return
|
|
||||||
await store[updateMethod](store.current.id, { currentStep: step })
|
|
||||||
await store[loadMethod](store.current.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const advanceStep = async () => {
|
|
||||||
if (!store.current) return
|
|
||||||
const nextStep = store.current.currentStep + 1
|
|
||||||
await store[updateMethod](store.current.id, { currentStep: nextStep })
|
|
||||||
await store[loadMethod](store.current.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
stepLabels,
|
|
||||||
currentStep,
|
|
||||||
entity,
|
|
||||||
init,
|
|
||||||
saveAndHold,
|
|
||||||
handleStepSelect,
|
|
||||||
advanceStep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { WorkflowConfig, WorkflowEntity } from '~/types/workflow'
|
|
||||||
|
|
||||||
export const receptionConfig: WorkflowConfig = {
|
|
||||||
entityName: 'reception',
|
|
||||||
apiResource: 'receptions',
|
|
||||||
steps: [
|
|
||||||
{ label: 'Réception' },
|
|
||||||
{ label: 'Pesée à plein', weighingMode: 'gross' },
|
|
||||||
{ label: 'Sélection réception' },
|
|
||||||
{ label: 'Pesée à vide', weighingMode: 'tare', isFinal: true }
|
|
||||||
],
|
|
||||||
weighingLabels: {
|
|
||||||
gross: 'Pesée à plein',
|
|
||||||
tare: 'Pesée à vide'
|
|
||||||
},
|
|
||||||
buildReceiptFilename: (entity: WorkflowEntity) => {
|
|
||||||
const rec = entity as any
|
|
||||||
return `${rec.identificationNumber ?? rec.id}_${rec.supplier?.name ?? 'fournisseur'}_${rec.licensePlate ?? 'immat'}.pdf`
|
|
||||||
},
|
|
||||||
routePrefix: '/reception',
|
|
||||||
toastPrefix: 'reception',
|
|
||||||
dateField: 'receptionDate'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RECEPTION_STEP_LABELS = receptionConfig.steps.map(s => s.label)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { WorkflowConfig, WorkflowEntity } from '~/types/workflow'
|
|
||||||
|
|
||||||
export const shipmentConfig: WorkflowConfig = {
|
|
||||||
entityName: 'shipment',
|
|
||||||
apiResource: 'shipments',
|
|
||||||
steps: [
|
|
||||||
{ label: 'Expédition' },
|
|
||||||
{ label: 'Pesée à vide', weighingMode: 'tare' },
|
|
||||||
{ label: 'Chargement' },
|
|
||||||
{ label: 'Pesée à plein', weighingMode: 'gross', isFinal: true }
|
|
||||||
],
|
|
||||||
weighingLabels: {
|
|
||||||
gross: 'Pesée à plein',
|
|
||||||
tare: 'Pesée à vide'
|
|
||||||
},
|
|
||||||
buildReceiptFilename: (entity: WorkflowEntity) => {
|
|
||||||
const ship = entity as any
|
|
||||||
return `${ship.identificationNumber ?? ship.id}_${ship.customer?.label ?? 'client'}_${ship.licensePlate ?? 'immat'}.pdf`
|
|
||||||
},
|
|
||||||
routePrefix: '/shipment',
|
|
||||||
toastPrefix: 'shipment',
|
|
||||||
dateField: 'shipmentDate'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SHIPMENT_STEP_LABELS = shipmentConfig.steps.map(s => s.label)
|
|
||||||
@@ -12,148 +12,21 @@
|
|||||||
"fetch": "Impossible de récupérer la réception.",
|
"fetch": "Impossible de récupérer la réception.",
|
||||||
"create": "Impossible de créer la réception.",
|
"create": "Impossible de créer la réception.",
|
||||||
"update": "Impossible de mettre à jour la réception.",
|
"update": "Impossible de mettre à jour la réception.",
|
||||||
"delete": "Impossible de supprimer la réception.",
|
|
||||||
"weight": "Impossible de récupérer la pesée."
|
|
||||||
},
|
|
||||||
"weight": {
|
|
||||||
"update": "Impossible de mettre à jour la pesée"
|
|
||||||
},
|
|
||||||
"shipment": {
|
|
||||||
"list": "Impossible de récupérer la liste des éxpeditions.",
|
|
||||||
"fetch": "Impossible de récupérer l'éxpeditions.",
|
|
||||||
"create": "Impossible de créer l'éxpeditions.",
|
|
||||||
"update": "Impossible de mettre à jour l'éxpeditions.",
|
|
||||||
"delete": "Impossible de supprimer l'expédition.",
|
|
||||||
"weigh": "Impossible de récupérer la pesée."
|
"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."
|
|
||||||
},
|
|
||||||
"bovine": {
|
|
||||||
"create": "Impossible d'enregistrer le 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": {
|
"auth": {
|
||||||
"login": "Identifiants invalides.",
|
"login": "Identifiants invalides.",
|
||||||
"users": "Impossible de récupérer les utilisateurs.",
|
"users": "Impossible de récupérer les utilisateurs.",
|
||||||
"logout": "Impossible de se déconnecter.",
|
"logout": "Impossible de se déconnecter."
|
||||||
"update": "Impossible de mettre à jour l'utilisateur.",
|
|
||||||
"create": "Impossible de créer l'utilisateur."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"reception": {
|
"reception": {
|
||||||
"create": "Réception créée avec succès",
|
"update": "Réception mise à jour avec succès."
|
||||||
"update": "Réception mise à jour avec succès.",
|
|
||||||
"delete": "Réception supprimée avec succès."
|
|
||||||
},
|
|
||||||
"shipment": {
|
|
||||||
"create": "Éxpedition créée avec succès",
|
|
||||||
"update": "Éxpedition mise à jour avec succès.",
|
|
||||||
"delete": "Expédition supprimée avec succès."
|
|
||||||
},
|
|
||||||
"supplier": {
|
|
||||||
"create": "Fournisseur créé avec succès.",
|
|
||||||
"update": "Fournisseur mis à jour avec succès."
|
|
||||||
},
|
|
||||||
"customer": {
|
|
||||||
"create": "Client créé avec succès.",
|
|
||||||
"update": "Client mis à jour avec succès."
|
|
||||||
},
|
|
||||||
"address": {
|
|
||||||
"create": "Adresse créée avec succès.",
|
|
||||||
"update": "Adresse mise à jour avec succès."
|
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"update": "Utilisateur mis à jour avec succès.",
|
|
||||||
"create": "Utilisateur créé avec succès.",
|
|
||||||
"login": "Connexion réussie.",
|
"login": "Connexion réussie.",
|
||||||
"logout": "Dé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."
|
|
||||||
},
|
|
||||||
"bovine": {
|
|
||||||
"create": "Bovin enregistré avec succès."
|
|
||||||
},
|
|
||||||
"weight": {
|
|
||||||
"update": "Pesée mis à jour"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,289 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen text-neutral-900 flex flex-col">
|
<div class="min-h-screen bg-white text-neutral-900">
|
||||||
<!-- HEADER -->
|
<header class="w-full border-b border-neutral-200 bg-primary-500">
|
||||||
<header class="w-full bg-primary-500 py-5 px-6">
|
<div class="flex w-full items-center px-6 py-4">
|
||||||
<div class="flex w-full items-center justify-between">
|
<NuxtLink to="/" class="flex items-center gap-3">
|
||||||
<!-- Burger (mobile) -->
|
<span
|
||||||
<button
|
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
||||||
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 flex-1 gap-8 text-2xl font-bold uppercase text-white">
|
||||||
<!-- NAV centré (desktop) -->
|
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
|
||||||
<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="route.path === '/'
|
:class="isExactActive ? 'opacity-100' : 'opacity-50'"
|
||||||
? '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="route.path.startsWith('/admin/user')
|
:class="isReceptionActive ? 'opacity-100' : 'opacity-50'"
|
||||||
? 'opacity-100'
|
|
||||||
: 'opacity-65 hover:opacity-100 transition'"
|
|
||||||
>
|
>
|
||||||
Utilisateurs
|
Reception
|
||||||
</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/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/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/bovin/bovin-list"
|
|
||||||
custom
|
|
||||||
v-slot="{ href, navigate }"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="href"
|
|
||||||
@click="navigate"
|
|
||||||
:class="route.path.startsWith('/admin/bovin')
|
|
||||||
? 'opacity-100'
|
|
||||||
: 'opacity-65 hover:opacity-100 transition'"
|
|
||||||
>
|
|
||||||
Bovins
|
|
||||||
</a>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
v-if="auth.isAdmin"
|
|
||||||
to="/scan"
|
|
||||||
custom
|
|
||||||
v-slot="{ href, navigate }"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="href"
|
|
||||||
@click="navigate"
|
|
||||||
:class="route.path.startsWith('/scan')
|
|
||||||
? 'opacity-100'
|
|
||||||
: 'opacity-65 hover:opacity-100 transition'"
|
|
||||||
>
|
|
||||||
Scanner
|
|
||||||
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center py-2 -my-2 text-xl leading-none transition hover:opacity-80"
|
class="ml-auto text-xl font-bold uppercase text-white 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"
|
@click="handleLogout"
|
||||||
>
|
>
|
||||||
Déconnexion
|
Déconnexion
|
||||||
</button>
|
</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>
|
|
||||||
<NuxtLink to="/scan" @click="closeMenu">
|
|
||||||
Scanner
|
|
||||||
</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="md:mx-auto w-full md:max-w-[1280px] mt-4 md:mt-16">
|
<main class="mx-auto w-full max-w-[1050px] px-6 pt-[90px] pb-0">
|
||||||
<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'
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const {version} = useAppVersion()
|
const isReceptionActive = computed(() => route.path.startsWith('/reception'))
|
||||||
|
|
||||||
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 () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
} finally {
|
} finally {
|
||||||
closeMenu()
|
|
||||||
await navigateTo('/login')
|
await navigateTo('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Garde-fou global : empêche les utilisateurs non-admin d'accéder aux pages
|
|
||||||
* sous /admin/*. Renvoie vers la home pour les utilisateurs authentifiés
|
|
||||||
* non-admin, et vers /login pour les non authentifiés.
|
|
||||||
*
|
|
||||||
* L'API back rejette de toute façon les actions admin avec un 403, mais ce
|
|
||||||
* middleware évite l'affichage des pages vides / en erreur quand un user
|
|
||||||
* tape directement l'URL /admin/...
|
|
||||||
*/
|
|
||||||
export default defineNuxtRouteMiddleware(async (to) => {
|
|
||||||
if (!to.path.startsWith('/admin')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth = useAuthStore()
|
|
||||||
await auth.ensureSession()
|
|
||||||
|
|
||||||
if (!auth.isAuthenticated) {
|
|
||||||
return navigateTo('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!auth.isAdmin) {
|
|
||||||
return navigateTo('/')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -2,26 +2,21 @@ 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/main.css', '~/assets/css/toast.css'],
|
css: ['~/assets/css/toast.css'],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE,
|
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
||||||
geoApiBase: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toast: {
|
toast: {
|
||||||
settings: {
|
settings: {
|
||||||
timeout: 10000,
|
timeout: 0,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
progressBar: false
|
progressBar: false
|
||||||
}
|
}
|
||||||
|
|||||||
184
frontend/package-lock.json
generated
184
frontend/package-lock.json
generated
@@ -7,7 +7,6 @@
|
|||||||
"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",
|
||||||
@@ -36,19 +35,6 @@
|
|||||||
"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",
|
||||||
@@ -77,7 +63,6 @@
|
|||||||
"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",
|
||||||
@@ -1078,6 +1063,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -1086,6 +1072,7 @@
|
|||||||
"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",
|
||||||
@@ -1099,6 +1086,7 @@
|
|||||||
"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"
|
||||||
@@ -1108,6 +1096,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1119,6 +1108,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1130,6 +1120,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1141,6 +1132,7 @@
|
|||||||
"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",
|
||||||
@@ -1163,6 +1155,7 @@
|
|||||||
"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"
|
||||||
@@ -1172,6 +1165,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -1180,6 +1174,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1191,6 +1186,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1202,6 +1198,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -1210,6 +1207,7 @@
|
|||||||
"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"
|
||||||
@@ -1222,6 +1220,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -1230,6 +1229,7 @@
|
|||||||
"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,6 +1242,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1254,6 +1255,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1262,47 +1264,6 @@
|
|||||||
"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",
|
||||||
@@ -2323,28 +2284,6 @@
|
|||||||
"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",
|
||||||
@@ -3028,7 +2967,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -4933,7 +4871,8 @@
|
|||||||
"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",
|
||||||
@@ -5283,7 +5222,6 @@
|
|||||||
"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",
|
||||||
@@ -5481,7 +5419,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -5519,6 +5456,7 @@
|
|||||||
"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",
|
||||||
@@ -5891,7 +5829,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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",
|
||||||
@@ -6005,7 +5942,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -6082,6 +6018,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -6200,7 +6137,6 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -6811,7 +6747,8 @@
|
|||||||
"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",
|
||||||
@@ -7322,6 +7259,7 @@
|
|||||||
"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"
|
||||||
@@ -7348,6 +7286,7 @@
|
|||||||
"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"
|
||||||
@@ -7357,6 +7296,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7368,6 +7308,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7379,6 +7320,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7390,6 +7332,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -7398,6 +7341,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7409,6 +7353,7 @@
|
|||||||
"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",
|
||||||
@@ -7425,6 +7370,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7448,6 +7394,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7459,6 +7406,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7556,7 +7504,8 @@
|
|||||||
"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",
|
||||||
@@ -7583,12 +7532,14 @@
|
|||||||
"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",
|
||||||
@@ -7629,6 +7580,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -7658,6 +7610,7 @@
|
|||||||
"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"
|
||||||
@@ -7673,6 +7626,7 @@
|
|||||||
"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"
|
||||||
@@ -7684,7 +7638,8 @@
|
|||||||
"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",
|
||||||
@@ -7951,6 +7906,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -8251,6 +8207,7 @@
|
|||||||
"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"
|
||||||
@@ -8266,6 +8223,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -8287,6 +8245,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -8614,8 +8573,7 @@
|
|||||||
"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",
|
||||||
@@ -8673,17 +8631,20 @@
|
|||||||
"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",
|
||||||
@@ -8761,6 +8722,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -9034,6 +8996,7 @@
|
|||||||
"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"
|
||||||
@@ -9118,6 +9081,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -9155,7 +9119,8 @@
|
|||||||
"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",
|
||||||
@@ -9501,7 +9466,8 @@
|
|||||||
"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",
|
||||||
@@ -10239,7 +10205,6 @@
|
|||||||
"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",
|
||||||
@@ -10507,6 +10472,7 @@
|
|||||||
"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",
|
||||||
@@ -10553,7 +10519,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -10626,6 +10591,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -10640,6 +10606,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -10666,6 +10633,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -10714,6 +10682,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -10828,7 +10797,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -10934,7 +10902,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -11484,7 +11451,6 @@
|
|||||||
"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"
|
||||||
@@ -11552,6 +11518,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -11606,6 +11573,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -11957,7 +11925,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -12517,6 +12484,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -12779,7 +12747,6 @@
|
|||||||
"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",
|
||||||
@@ -13119,6 +13086,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -13188,7 +13156,6 @@
|
|||||||
"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"
|
||||||
@@ -13630,6 +13597,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -13655,7 +13623,6 @@
|
|||||||
"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",
|
||||||
@@ -14012,7 +13979,6 @@
|
|||||||
"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",
|
||||||
@@ -14048,7 +14014,6 @@
|
|||||||
"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",
|
||||||
@@ -14069,7 +14034,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -14121,6 +14085,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -14386,6 +14351,7 @@
|
|||||||
"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,7 +11,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form :class="{ submitted }" @submit.prevent="validate">
|
|
||||||
<div class="flex items-center justify-between relative">
|
|
||||||
<div class="flex flex-row absolute -left-[60px]">
|
|
||||||
<Icon
|
|
||||||
@click="router.push('/admin/bovin/bovin-list')"
|
|
||||||
name="gg:arrow-left-o"
|
|
||||||
size="40"
|
|
||||||
class="cursor-pointer text-primary-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
|
||||||
{{ route.params.id ? 'Modifications du type bovin' : 'Ajout d\'un type bovin' }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]">
|
|
||||||
<UiTextInput label="Nom du bovin" id="bovin-label" v-model="form.label" required />
|
|
||||||
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" required />
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<UiButton
|
|
||||||
type="submit"
|
|
||||||
:disabled="isLoading || isHydrating"
|
|
||||||
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
|
||||||
@click="submitted = true"
|
|
||||||
>
|
|
||||||
Valider
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Type de bovin' })
|
|
||||||
|
|
||||||
import {createBovin, getBovin, updateBovin} from "~/services/bovine-type";
|
|
||||||
import type {BovineTypeData, BovinFormData} from "~/services/dto/bovine-type-data";
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const isHydrating = ref(false)
|
|
||||||
const submitted = ref(false)
|
|
||||||
const idBovin = computed(() => resolveId(route.params.id))
|
|
||||||
const isEdit = computed(() => idBovin.value !== null)
|
|
||||||
|
|
||||||
function resolveId(param: unknown) {
|
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
|
||||||
if (!idStr) return null
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = reactive<BovinFormData>({
|
|
||||||
label: '',
|
|
||||||
code: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const hydrateFromBovin = (bovin: BovineTypeData | null) => {
|
|
||||||
if (!bovin) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isHydrating.value = true
|
|
||||||
form.label = bovin.label ?? ''
|
|
||||||
form.code = bovin.code ?? ''
|
|
||||||
isHydrating.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => idBovin.value,
|
|
||||||
async (id) => {
|
|
||||||
if (id === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
const bovin = await getBovin(id)
|
|
||||||
hydrateFromBovin(bovin)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
async function validate() {
|
|
||||||
if (isLoading.value || isHydrating.value) return
|
|
||||||
|
|
||||||
const normalizedBovinCode = form.code.trim()
|
|
||||||
const normalizedBovinLabel = form.label.trim()
|
|
||||||
|
|
||||||
|
|
||||||
const basePayload = {
|
|
||||||
label: normalizedBovinLabel,
|
|
||||||
code: normalizedBovinCode
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
if (isEdit.value && idBovin.value !== null) {
|
|
||||||
await updateBovin(idBovin.value, basePayload)
|
|
||||||
} else {
|
|
||||||
await createBovin(basePayload)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function navigate(){
|
|
||||||
return router.push("/admin/bovin/list")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="px-[86px]">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des types bovins</h1>
|
|
||||||
<NuxtLink
|
|
||||||
v-if="auth.isAdmin"
|
|
||||||
to="/admin/bovin"
|
|
||||||
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-6 rounded hover:opacity-80 gap-2"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:plus" size="28" />
|
|
||||||
Ajouter
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="auth.isAdmin" class="mt-6 mb-16">
|
|
||||||
<UiDataTable
|
|
||||||
v-model:page="page"
|
|
||||||
v-model:per-page="perPage"
|
|
||||||
:columns="columns"
|
|
||||||
:items="items"
|
|
||||||
:total-items="totalItems"
|
|
||||||
:loading="loading"
|
|
||||||
row-clickable
|
|
||||||
@row-click="goToBovin"
|
|
||||||
>
|
|
||||||
<template #header-label>
|
|
||||||
<UiTextInput v-model="filters.label" placeholder="Nom" size="compact" />
|
|
||||||
</template>
|
|
||||||
<template #header-code>
|
|
||||||
<UiTextInput v-model="filters.code" placeholder="Code" size="compact" />
|
|
||||||
</template>
|
|
||||||
</UiDataTable>
|
|
||||||
</div>
|
|
||||||
<div v-else class="mt-6 border border-slate-200 mb-16 px-4 py-6 text-slate-400">
|
|
||||||
Accès réservé aux administrateurs.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Types de bovins' })
|
|
||||||
|
|
||||||
import type { BovineTypeData } from '~/services/dto/bovine-type-data'
|
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
const { items, totalItems, page, perPage, filters, loading, reload } =
|
|
||||||
useDataTableServerState<BovineTypeData>(
|
|
||||||
'bovine_types',
|
|
||||||
{
|
|
||||||
label: '',
|
|
||||||
code: ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ key: 'label', label: 'Nom' },
|
|
||||||
{ key: 'code', label: 'Code' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const goToBovin = (bovin: BovineTypeData) => {
|
|
||||||
if (!auth.isAdmin) return
|
|
||||||
router.push(`/admin/bovin/${bovin.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (auth.isAdmin) reload()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
|
|
||||||
<template>
|
|
||||||
<form :class="{ submitted }" @submit.prevent="validate">
|
|
||||||
<div class="flex items-center justify-between relative">
|
|
||||||
<div class="flex flex-row absolute -left-[60px]">
|
|
||||||
<Icon
|
|
||||||
@click="router.push('/admin/carrier/carrier-list')"
|
|
||||||
name="gg:arrow-left-o"
|
|
||||||
size="40"
|
|
||||||
class="cursor-pointer text-primary-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
|
||||||
{{ route.params.id ? 'Modification du transporteur' : 'Ajout d\'un transporteur' }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]">
|
|
||||||
<UiTextInput
|
|
||||||
label="Nom du transporteur"
|
|
||||||
id="carrier-name"
|
|
||||||
v-model="form.name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UiTextInput
|
|
||||||
label="Code transporteur"
|
|
||||||
id="code-id"
|
|
||||||
v-model="form.code"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<UiButton
|
|
||||||
type="submit"
|
|
||||||
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
|
||||||
@click="submitted = true"
|
|
||||||
>
|
|
||||||
Valider
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Transporteur' })
|
|
||||||
|
|
||||||
import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier";
|
|
||||||
import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data";
|
|
||||||
import {computed} from "vue";
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
const idCarrier = computed(() => resolveId(route.params.id))
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const isHydrating = ref(false)
|
|
||||||
const submitted = ref(false)
|
|
||||||
|
|
||||||
const resolveId = (param: unknown) => {
|
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
|
||||||
if (!idStr) return null
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = reactive<CarrierFormData>({
|
|
||||||
code:'',
|
|
||||||
name:''
|
|
||||||
})
|
|
||||||
|
|
||||||
const hydrateFromUser = (carrier: CarrierData | null) => {
|
|
||||||
if (!carrier) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isHydrating.value = true
|
|
||||||
form.name = carrier.name ?? ''
|
|
||||||
form.code = carrier.code ?? ''
|
|
||||||
isHydrating.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => idCarrier.value,
|
|
||||||
async (id) => {
|
|
||||||
if (id === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
const user = await getCarrier(id)
|
|
||||||
hydrateFromUser(user)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
async function validate() {
|
|
||||||
|
|
||||||
const normalizedCarrierCode = form.code.trim()
|
|
||||||
const normalizedCarrierName = form.name.trim()
|
|
||||||
|
|
||||||
const basePayload = {
|
|
||||||
name: normalizedCarrierName,
|
|
||||||
code: normalizedCarrierCode
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(idCarrier.value){
|
|
||||||
await updateCarrier(idCarrier.value, basePayload)
|
|
||||||
return
|
|
||||||
}else{
|
|
||||||
await createCarrier(basePayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigate(){
|
|
||||||
router.push("/admin/carrier/carrier-list")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="px-[86px]">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-4xl font-bold uppercase text-primary-500">listes des transporteurs</h1>
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/carrier"
|
|
||||||
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-6 rounded hover:opacity-80 gap-2"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:plus" size="28" />
|
|
||||||
Ajouter
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 mb-16">
|
|
||||||
<UiDataTable
|
|
||||||
v-model:page="page"
|
|
||||||
v-model:per-page="perPage"
|
|
||||||
:columns="columns"
|
|
||||||
:items="items"
|
|
||||||
:total-items="totalItems"
|
|
||||||
:loading="loading"
|
|
||||||
row-clickable
|
|
||||||
@row-click="goToCarrier"
|
|
||||||
>
|
|
||||||
<template #header-name>
|
|
||||||
<UiTextInput v-model="filters.name" placeholder="Label" size="compact" />
|
|
||||||
</template>
|
|
||||||
<template #header-code>
|
|
||||||
<UiTextInput v-model="filters.code" placeholder="Code" size="compact" />
|
|
||||||
</template>
|
|
||||||
</UiDataTable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Transporteurs' })
|
|
||||||
|
|
||||||
import type { CarrierData } from '~/services/dto/carrier-data'
|
|
||||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const { items, totalItems, page, perPage, filters, loading, reload } =
|
|
||||||
useDataTableServerState<CarrierData>(
|
|
||||||
'carriers',
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
code: ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ key: 'name', label: 'Label' },
|
|
||||||
{ key: 'code', label: 'Code' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const goToCarrier = (carrier: CarrierData) => {
|
|
||||||
router.push(`/admin/carrier/${carrier.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(reload)
|
|
||||||
</script>
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form :class="{ submitted }" @submit.prevent="validate">
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<div class="flex flex-row absolute -left-[60px] ">
|
|
||||||
<Icon @click="router.push('/admin/customer/customer-list')" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
|
||||||
{{ customerId ? "Modification du client" : "Ajout d'un client" }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
|
||||||
<UiTextInput id="customer-name" v-model="form.name" label="Nom du client" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
|
||||||
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
|
||||||
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!customerId" class="flex flex-cols-3 justify-between mb-11">
|
|
||||||
<UiTextInput id="address-street" v-model="addressForm.street" label="Rue" wrapper-class="w-[280px]" required />
|
|
||||||
<UiTextInput id="address-street2" v-model="addressForm.street2" label="Complément" wrapper-class="w-[280px]" />
|
|
||||||
<UiTextInput id="address-country" v-model="addressForm.countryCode" label="Pays (code)" wrapper-class="w-[280px]" />
|
|
||||||
</div>
|
|
||||||
<div v-if="!customerId" class="flex flex-cols-3 justify-between mb-11">
|
|
||||||
<UiTextInput id="address-postalCode" v-model="addressForm.postalCode" label="Code postal" wrapper-class="w-[280px]" required />
|
|
||||||
<UiSelect id="address-city" v-model="addressForm.city" label="Ville"
|
|
||||||
:options="communeOptions" :loading="isLoadingCities"
|
|
||||||
wrapper-class="w-[280px]" required />
|
|
||||||
<div class="w-[280px]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<UiButton
|
|
||||||
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
|
||||||
type="submit"
|
|
||||||
:disabled="isLoading || !auth.isAdmin"
|
|
||||||
@click="submitted = true"
|
|
||||||
>
|
|
||||||
<Icon :name="customerId ? '' : 'mdi:plus'" size="28" />
|
|
||||||
{{ customerId ? "Valider" : "Ajouter" }}
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="customerId">
|
|
||||||
<div class="flex items-center justify-between mb-7">
|
|
||||||
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du client</h2>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto mb-11 text-primary-700">
|
|
||||||
<table class="w-full border-collapse text-primary-700">
|
|
||||||
<thead>
|
|
||||||
<tr class="text-left border bg-slate-100 border-gray-200">
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Rue</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Complément</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Code postal</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Ville</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Pays</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template v-if="form.addresses.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="py-4 text-slate-400">
|
|
||||||
Aucune adresse.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<tr
|
|
||||||
v-for="(address, index) in form.addresses"
|
|
||||||
:key="address.id ?? index"
|
|
||||||
class="border border-gray-100 hover:bg-slate-50"
|
|
||||||
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
|
||||||
@click="goToEditAddress(address.id ?? null)"
|
|
||||||
>
|
|
||||||
<td class="py-3 px-4">{{ address.street || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.street2 || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.postalCode || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.city || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.countryCode || "—" }}</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<UiButton
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center justify-center text-xl gap-2 text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
|
||||||
:disabled="customerId === null || !auth.isAdmin"
|
|
||||||
@click="goToAddAddress"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:plus" size="28" />
|
|
||||||
Ajouter
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Client' })
|
|
||||||
|
|
||||||
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 {createAddress, type AddressPayload} from "~/services/address"
|
|
||||||
import {getCommunesByPostalCode, type CommuneData} from "~/services/geo"
|
|
||||||
import {useAuthStore} from "~/stores/auth"
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
const resolveId = (param: unknown) => {
|
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
|
||||||
if (!idStr) return null
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
const customerId = computed(() => resolveId(route.params.id))
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const submitted = ref(false)
|
|
||||||
const form = reactive<CustomerFormData>({
|
|
||||||
name: "",
|
|
||||||
phone: "",
|
|
||||||
email: "",
|
|
||||||
addresses: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Address form (creation mode only)
|
|
||||||
const addressForm = reactive<AddressPayload>({
|
|
||||||
street: "", street2: null, postalCode: "", city: "", countryCode: "FR",
|
|
||||||
})
|
|
||||||
const communes = ref<CommuneData[]>([])
|
|
||||||
const isLoadingCities = ref(false)
|
|
||||||
const communeOptions = computed(() => communes.value.map(c => ({ value: c.nom, label: c.nom })))
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
watch(() => addressForm.postalCode, (cp) => {
|
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
|
||||||
if (!cp || cp.length < 5) { communes.value = []; addressForm.city = ''; return }
|
|
||||||
if (cp.length === 5) {
|
|
||||||
debounceTimer = setTimeout(async () => {
|
|
||||||
isLoadingCities.value = true
|
|
||||||
try {
|
|
||||||
communes.value = await getCommunesByPostalCode(cp)
|
|
||||||
if (communes.value.length === 1) addressForm.city = communes.value[0].nom
|
|
||||||
else addressForm.city = ''
|
|
||||||
} finally { isLoadingCities.value = false }
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const goToAddAddress = () => {
|
|
||||||
if (customerId.value === null || !auth.isAdmin) return
|
|
||||||
router.push({
|
|
||||||
path: "/admin/customer/address",
|
|
||||||
query: {
|
|
||||||
customerId: String(customerId.value),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToEditAddress = (addressId: number | null) => {
|
|
||||||
if (customerId.value === null || addressId === null || !auth.isAdmin) return
|
|
||||||
router.push({
|
|
||||||
path: "/admin/customer/address",
|
|
||||||
query: {
|
|
||||||
customerId: String(customerId.value),
|
|
||||||
addressId: String(addressId),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const hydrateFromCustomer = (customer: CustomerData | null) => {
|
|
||||||
if (!customer) return
|
|
||||||
form.name = customer.name ?? ""
|
|
||||||
form.phone = customer.phone ?? ""
|
|
||||||
form.email = customer.email ?? ""
|
|
||||||
if (!Array.isArray(customer.addresses) || customer.addresses.length === 0) {
|
|
||||||
form.addresses = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (typeof customer.addresses[0] === "string") {
|
|
||||||
form.addresses = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form.addresses = customer.addresses.map((address) => ({
|
|
||||||
id: address.id ?? null,
|
|
||||||
street: address.street ?? "",
|
|
||||||
street2: address.street2 ?? null,
|
|
||||||
postalCode: address.postalCode ?? "",
|
|
||||||
city: address.city ?? "",
|
|
||||||
countryCode: address.countryCode ?? "",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => customerId.value,
|
|
||||||
async (id) => {
|
|
||||||
if (id === null) return
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
const customer = await getCustomer(id)
|
|
||||||
hydrateFromCustomer(customer)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function validate() {
|
|
||||||
if (isLoading.value) return
|
|
||||||
if (!auth.isAdmin) return
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const name = form.name.trim()
|
|
||||||
const phone = form.phone?.trim() || null
|
|
||||||
const email = form.email?.trim() || null
|
|
||||||
|
|
||||||
const customerPayload: CustomerPayload = {
|
|
||||||
name,
|
|
||||||
phone,
|
|
||||||
email,
|
|
||||||
}
|
|
||||||
let targetId: number | null = null
|
|
||||||
|
|
||||||
if (customerId.value !== null) {
|
|
||||||
await updateCustomer(customerId.value, customerPayload)
|
|
||||||
targetId = customerId.value
|
|
||||||
} else {
|
|
||||||
const addressData = await createAddress({ ...addressForm })
|
|
||||||
const addressIRI = `/api/addresses/${addressData.id}`
|
|
||||||
const creationPayload = {
|
|
||||||
...customerPayload,
|
|
||||||
addresses: [addressIRI],
|
|
||||||
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
|
||||||
}
|
|
||||||
const created = await createCustomer(creationPayload)
|
|
||||||
targetId = created.id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetId !== null) {
|
|
||||||
await router.push(`/admin/customer/${targetId}`)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Address type="customer" :address="address" @validate="validate"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Adresse client' })
|
|
||||||
|
|
||||||
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>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="px-[86px]">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des clients</h1>
|
|
||||||
<NuxtLink
|
|
||||||
v-if="auth.isAdmin"
|
|
||||||
to="/admin/customer"
|
|
||||||
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-6 rounded hover:opacity-80 gap-2"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:plus" size="28" />
|
|
||||||
Ajouter
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="auth.isAdmin" class="mt-6 mb-16">
|
|
||||||
<UiDataTable
|
|
||||||
v-model:page="page"
|
|
||||||
v-model:per-page="perPage"
|
|
||||||
:columns="columns"
|
|
||||||
:items="items"
|
|
||||||
:total-items="totalItems"
|
|
||||||
:loading="loading"
|
|
||||||
row-clickable
|
|
||||||
@row-click="goToCustomer"
|
|
||||||
>
|
|
||||||
<template #header-name>
|
|
||||||
<UiTextInput v-model="filters.name" placeholder="Nom" size="compact" />
|
|
||||||
</template>
|
|
||||||
<template #header-phone>
|
|
||||||
<UiTextInput v-model="filters.phone" placeholder="Téléphone" size="compact" />
|
|
||||||
</template>
|
|
||||||
<template #header-email>
|
|
||||||
<UiTextInput v-model="filters.email" placeholder="Mail" size="compact" />
|
|
||||||
</template>
|
|
||||||
<template #header-createdBy.username>
|
|
||||||
<UiTextInput v-model="filters['createdBy.username']" placeholder="Créé par" size="compact" />
|
|
||||||
</template>
|
|
||||||
</UiDataTable>
|
|
||||||
</div>
|
|
||||||
<div v-else class="mt-6 border border-slate-200 mb-16 px-4 py-6 text-slate-400">
|
|
||||||
Accès réservé aux administrateurs.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Clients' })
|
|
||||||
|
|
||||||
import type { CustomerData } from '~/services/dto/customer-data'
|
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
const { items, totalItems, page, perPage, filters, loading, reload } =
|
|
||||||
useDataTableServerState<CustomerData>(
|
|
||||||
'customers',
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
'createdBy.username': ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ key: 'name', label: 'Nom' },
|
|
||||||
{ key: 'phone', label: 'Téléphone' },
|
|
||||||
{ key: 'email', label: 'Mail' },
|
|
||||||
{ key: 'createdBy.username', label: 'Créé par' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const goToCustomer = (customer: CustomerData) => {
|
|
||||||
if (!auth.isAdmin) return
|
|
||||||
router.push(`/admin/customer/${customer.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (auth.isAdmin) reload()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form :class="{ submitted }" @submit.prevent="validate">
|
|
||||||
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<div class="flex flex-row absolute -left-[60px] ">
|
|
||||||
<Icon @click="router.push('/admin/supplier/supplier-list')" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
|
||||||
{{ supplierId ? "Modification du fournisseur" : "Ajout d'un fournisseur" }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
|
||||||
<UiTextInput id="supplier-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
|
||||||
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
|
||||||
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!supplierId" class="flex flex-cols-3 justify-between mb-11">
|
|
||||||
<UiTextInput id="address-street" v-model="addressForm.street" label="Rue" wrapper-class="w-[280px]" required />
|
|
||||||
<UiTextInput id="address-street2" v-model="addressForm.street2" label="Complément" wrapper-class="w-[280px]" />
|
|
||||||
<UiTextInput id="address-country" v-model="addressForm.countryCode" label="Pays (code)" wrapper-class="w-[280px]" />
|
|
||||||
</div>
|
|
||||||
<div v-if="!supplierId" class="flex flex-cols-3 justify-between mb-11">
|
|
||||||
<UiTextInput id="address-postalCode" v-model="addressForm.postalCode" label="Code postal" wrapper-class="w-[280px]" required />
|
|
||||||
<UiSelect id="address-city" v-model="addressForm.city" label="Ville"
|
|
||||||
:options="communeOptions" :loading="isLoadingCities"
|
|
||||||
wrapper-class="w-[280px]" required />
|
|
||||||
<div class="w-[280px]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<UiButton
|
|
||||||
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
|
||||||
type="submit"
|
|
||||||
:disabled="isLoading || !auth.isAdmin"
|
|
||||||
@click="submitted = true"
|
|
||||||
>
|
|
||||||
<Icon :name="supplierId ? '' : 'mdi:plus'" size="28" />
|
|
||||||
{{ supplierId ? "Valider" : "Ajouter" }}
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="supplierId">
|
|
||||||
<div class="flex items-center justify-between mb-7">
|
|
||||||
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du fournisseur</h2>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto mb-11 text-primary-700">
|
|
||||||
<table class="w-full border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr class="text-left border bg-slate-100 border-gray-200">
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Rue</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Complément</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Code postal</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Ville</th>
|
|
||||||
<th class="py-3 px-4 text-sm uppercase">Pays</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template v-if="form.addresses.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="py-4 text-slate-400">
|
|
||||||
Aucune adresse.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<tr
|
|
||||||
v-for="(address, index) in form.addresses"
|
|
||||||
:key="address.id ?? index"
|
|
||||||
class="border border-gray-100 hover:bg-slate-50"
|
|
||||||
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
|
||||||
@click="goToEditAddress(address.id ?? null)"
|
|
||||||
>
|
|
||||||
<td class="py-3 px-4">{{ address.street || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.street2 || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.postalCode || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.city || "—" }}</td>
|
|
||||||
<td class="py-3 px-4">{{ address.countryCode || "—" }}</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<UiButton
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center justify-center text-xl gap-2 text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
|
||||||
:disabled="supplierId === null || !auth.isAdmin"
|
|
||||||
@click="goToAddAddress"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:plus" size="28" />
|
|
||||||
Ajouter
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Fournisseur' })
|
|
||||||
|
|
||||||
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 {createAddress, type AddressPayload} from "~/services/address"
|
|
||||||
import {getCommunesByPostalCode, type CommuneData} from "~/services/geo"
|
|
||||||
import {useAuthStore} from "~/stores/auth"
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
const resolveId = (param: unknown) => {
|
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
|
||||||
if (!idStr) return null
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
const supplierId = computed(() => resolveId(route.params.id))
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const submitted = ref(false)
|
|
||||||
const form = reactive<SupplierFormData>({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
addresses: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Address form (creation mode only)
|
|
||||||
const addressForm = reactive<AddressPayload>({
|
|
||||||
street: "", street2: null, postalCode: "", city: "", countryCode: "FR",
|
|
||||||
})
|
|
||||||
const communes = ref<CommuneData[]>([])
|
|
||||||
const isLoadingCities = ref(false)
|
|
||||||
const communeOptions = computed(() => communes.value.map(c => ({ value: c.nom, label: c.nom })))
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
watch(() => addressForm.postalCode, (cp) => {
|
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
|
||||||
if (!cp || cp.length < 5) { communes.value = []; addressForm.city = ''; return }
|
|
||||||
if (cp.length === 5) {
|
|
||||||
debounceTimer = setTimeout(async () => {
|
|
||||||
isLoadingCities.value = true
|
|
||||||
try {
|
|
||||||
communes.value = await getCommunesByPostalCode(cp)
|
|
||||||
if (communes.value.length === 1) addressForm.city = communes.value[0].nom
|
|
||||||
else addressForm.city = ''
|
|
||||||
} finally { isLoadingCities.value = false }
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const goToAddAddress = () => {
|
|
||||||
if (supplierId.value === null || !auth.isAdmin) return
|
|
||||||
router.push({
|
|
||||||
path: "/admin/supplier/address",
|
|
||||||
query: {
|
|
||||||
supplierId: String(supplierId.value),
|
|
||||||
fromSupplier: "1",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const goToEditAddress = (addressId: number | null) => {
|
|
||||||
if (supplierId.value === null || addressId === null || !auth.isAdmin) return
|
|
||||||
router.push({
|
|
||||||
path: "/admin/supplier/address",
|
|
||||||
query: {
|
|
||||||
supplierId: String(supplierId.value),
|
|
||||||
addressId: String(addressId),
|
|
||||||
fromSupplier: "1",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const hydrateFromSupplier = (supplier: SupplierData | null) => {
|
|
||||||
if (!supplier) return
|
|
||||||
form.name = supplier.name ?? ""
|
|
||||||
form.email = supplier.email ?? ""
|
|
||||||
form.phone = supplier.phone ?? ""
|
|
||||||
if (!Array.isArray(supplier.addresses) || supplier.addresses.length === 0) {
|
|
||||||
form.addresses = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (typeof supplier.addresses[0] === "string") {
|
|
||||||
form.addresses = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form.addresses = supplier.addresses.map((address) => ({
|
|
||||||
id: address.id ?? null,
|
|
||||||
street: address.street ?? "",
|
|
||||||
street2: address.street2 ?? null,
|
|
||||||
postalCode: address.postalCode ?? "",
|
|
||||||
city: address.city ?? "",
|
|
||||||
countryCode: address.countryCode ?? "",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => supplierId.value,
|
|
||||||
async (id) => {
|
|
||||||
if (id === null) return
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
const supplier = await getSupplier(id)
|
|
||||||
hydrateFromSupplier(supplier)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function validate() {
|
|
||||||
if (isLoading.value) return
|
|
||||||
if (!auth.isAdmin) return
|
|
||||||
isLoading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const name = form.name.trim()
|
|
||||||
const email = (form.email ?? "").trim() || null
|
|
||||||
const phone = (form.phone ?? "").trim() || null
|
|
||||||
|
|
||||||
const supplierPayload: SupplierPayload = {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
phone,
|
|
||||||
}
|
|
||||||
let targetId: number | null = null
|
|
||||||
|
|
||||||
if (supplierId.value !== null) {
|
|
||||||
await updateSupplier(supplierId.value, supplierPayload)
|
|
||||||
targetId = supplierId.value
|
|
||||||
} else {
|
|
||||||
const addressData = await createAddress({ ...addressForm })
|
|
||||||
const addressIRI = `/api/addresses/${addressData.id}`
|
|
||||||
const creationPayload = {
|
|
||||||
...supplierPayload,
|
|
||||||
addresses: [addressIRI],
|
|
||||||
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
|
||||||
}
|
|
||||||
const created = await createSupplier(creationPayload)
|
|
||||||
targetId = created.id
|
|
||||||
}
|
|
||||||
|
|
||||||
await router.push(`/admin/supplier/${targetId}`)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Address type="supplier" :address="address" @validate="validate"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({ title: 'Adresse fournisseur' })
|
|
||||||
|
|
||||||
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>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user