Compare commits
92 Commits
main
...
886b90215a
| Author | SHA1 | Date | |
|---|---|---|---|
| 886b90215a | |||
| 265e85b8f4 | |||
|
|
579bdba65b | ||
| b1c3952d09 | |||
| b4e024590e | |||
| 74af5d30ba | |||
| b905cb66aa | |||
|
|
ab6de16319 | ||
| 800ab1d432 | |||
| 5f4139fde3 | |||
| 0181d72144 | |||
| bffdc88701 | |||
|
|
fade51d3ee | ||
| 9ca0a7511b | |||
| 42c5a3b2d2 | |||
|
|
d3dfde7060 | ||
| 90c2cfc665 | |||
|
|
9fc3e2f9bc | ||
| 329bb4cee5 | |||
|
|
d3af654858 | ||
| 168d8c78eb | |||
|
|
338d903cef | ||
| 42ce1e2d08 | |||
|
|
0d0aa788db | ||
| c010bdc262 | |||
|
|
0e905bfcbe | ||
| e6bb4ddf6a | |||
| 299ea84e87 | |||
| bb0b0092da | |||
| 33d21f6ae6 | |||
| 98ee62294d | |||
| 820386b87b | |||
| c17f7aa08a | |||
| 4a0d38d307 | |||
| 28d5bc7599 | |||
| e9948d6ac3 | |||
| 69d047fca0 | |||
| 80d87b7c9b | |||
| ba4375f609 | |||
| a69556c554 | |||
| e249d44e78 | |||
| e8189a4d04 | |||
| 081c2ef403 | |||
| 5fd2ab8470 | |||
| 13e8698673 | |||
| a34bdbfe8d | |||
| d8e1cdc72c | |||
| 2c54a8c950 | |||
| 7c85d91c78 | |||
| 149bced1c5 | |||
| 086279f962 | |||
| 1ce6357c1d | |||
| 9ae073e69e | |||
| 2cd05a39ba | |||
| 66e9c52914 | |||
| 80d6d72e37 | |||
| b883546575 | |||
| 038596951d | |||
| ac5a3493e7 | |||
| 7dc4fdd1c0 | |||
| 5395dfefda | |||
| c4f4107512 | |||
| 22f26ddb38 | |||
| d3289c8497 | |||
| 9f589bc86c | |||
| 66274e239b | |||
| f93a867dd4 | |||
| 744d8a4088 | |||
| cf693c0304 | |||
| 44bff2a4e5 | |||
| 6c1f14ae4d | |||
| 6f2218d2c9 | |||
| 9596d94617 | |||
| f99e5cd386 | |||
| 8f5730c3f6 | |||
| 42fafc5d39 | |||
| cc83242883 | |||
| 2d3ce2ca43 | |||
| 94ea49587a | |||
| 4a77449a41 | |||
| 9fb0fc12b8 | |||
| 14960d5e87 | |||
| ecb6f25159 | |||
| 566e7f132a | |||
| a2d20dafb1 | |||
| 6fc72d180a | |||
| c082224c7d | |||
| 74a0120883 | |||
| b0f4004d11 | |||
| c759194b83 | |||
| 5f0703811f | |||
| 8ea211835f |
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{compose.yaml,compose.*.yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
22
.env
Normal file
22
.env
Normal file
@@ -0,0 +1,22 @@
|
||||
APP_ENV=
|
||||
APP_DEBUG=
|
||||
APP_SECRET=
|
||||
|
||||
DEFAULT_URI=
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN=
|
||||
###< nelmio/cors-bundle ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
JWT_SECRET_KEY=
|
||||
JWT_PUBLIC_KEY=
|
||||
JWT_PASSPHRASE=
|
||||
COOKIE_SECURE=
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
||||
# ADAPTER avec la vraie BDD (pas de variables docker)
|
||||
DATABASE_URL=
|
||||
|
||||
PONT_BASCULE_BYPASS=
|
||||
PONT_BASCULE_URL=
|
||||
3
.env.test
Normal file
3
.env.test
Normal file
@@ -0,0 +1,3 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
20
.gitattributes
vendored
Normal file
20
.gitattributes
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Force LF in repo
|
||||
* text=auto eol=lf
|
||||
|
||||
# (optionnel) scripts
|
||||
*.sh text eol=lf
|
||||
*.bash text eol=lf
|
||||
|
||||
# (optionnel) configs
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.env text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
|
||||
# (optionnel) frontend
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.js text eol=lf
|
||||
*.vue text eol=lf
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
23
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
23
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
|
||||
name: "Merge Request"
|
||||
about: "Template de MR"
|
||||
title: "[#NUMERO_TICKET] TITRE TICKET"
|
||||
ref: "main"
|
||||
|
||||
---
|
||||
|
||||
| Numéro du ticket | Titre du ticket |
|
||||
|------------------|-----------------|
|
||||
| | |
|
||||
|
||||
## Description de la PR
|
||||
|
||||
## Modification du .env
|
||||
|
||||
## Check list
|
||||
|
||||
- [ ] Pas de régression
|
||||
- [ ] TU/TI/TF rédigée
|
||||
- [ ] TU/TI/TF OK
|
||||
- [ ] CHANGELOG modifié
|
||||
65
.gitea/workflows/auto-tag-develop.yml
Normal file
65
.gitea/workflows/auto-tag-develop.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Auto Tag Develop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
persist-credentials: true
|
||||
|
||||
- name: Create next tag from config/version.yaml
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Skip if current commit already has a vX.Y.Z tag
|
||||
if git tag --points-at HEAD | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "Tag already exists on this commit. Skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
changed_version=false
|
||||
if git diff --name-only "${{ gitea.event.before }}" "${{ gitea.event.after }}" | grep -q '^config/version\.yaml$'; then
|
||||
changed_version=true
|
||||
fi
|
||||
|
||||
read_version() {
|
||||
awk -F': *' '/app\.version:/{print $2}' config/version.yaml | tr -d '[:space:]' | tr -d "'\""
|
||||
}
|
||||
|
||||
if $changed_version; then
|
||||
version="$(read_version)"
|
||||
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Invalid version in version.yaml: $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
last_tag="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)"
|
||||
if [ -z "$last_tag" ]; then
|
||||
version="0.1.0"
|
||||
else
|
||||
base="${last_tag#v}"
|
||||
IFS='.' read -r major minor patch <<< "$base"
|
||||
version="${major}.${minor}.$((patch + 1))"
|
||||
fi
|
||||
|
||||
printf "parameters:\\n app.version: '%s'\\n" "$version" > config/version.yaml
|
||||
git config user.name "gitea-actions"
|
||||
git config user.email "gitea-actions@local"
|
||||
git add config/version.yaml
|
||||
git commit -m "chore: bump version to v$version" || true
|
||||
git push origin develop || true
|
||||
fi
|
||||
|
||||
tag="v$version"
|
||||
git tag "$tag"
|
||||
git push origin "$tag"
|
||||
65
.gitea/workflows/release-artefact.yml
Normal file
65
.gitea/workflows/release-artefact.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Build Release Artefact
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v0.0.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.4"
|
||||
extensions: mbstring, intl, pdo_pgsql, xml, curl, zip, gd
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install backend deps (prod)
|
||||
env:
|
||||
APP_ENV: prod
|
||||
APP_DEBUG: "0"
|
||||
run: composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
|
||||
|
||||
- name: Build frontend (static)
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ 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 }}
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.prod
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
||||
/LOG/
|
||||
/config/jwt/*.pem
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.cache/
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> docker ###
|
||||
docker/.env.docker.local
|
||||
###< docker ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
/config/jwt/*.pem
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
9
.idea/.gitignore
generated
vendored
Normal file
9
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AskMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Ask2AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EditMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
19
.idea/dataSources.xml
generated
Normal file
19
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?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>
|
||||
<data-source source="LOCAL" name="Ferme recette" uuid="ae622167-c834-4e7b-87a5-c1721036f5dc">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/data_source_mapping.xml
generated
Normal file
10
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_1.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_2.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_3.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_4.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/db-forest-config.xml
generated
Normal file
6
.idea/db-forest-config.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="db-tree-configuration">
|
||||
<option name="data" value="---------------------------------------- 1:0:f407a514-c6b4-4b26-9555-445a85892502 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc " />
|
||||
</component>
|
||||
</project>
|
||||
163
.idea/ferme.iml
generated
Normal file
163
.idea/ferme.iml
generated
Normal file
@@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="App\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/public/bundles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/var" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/doctrine-common" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/doctrine-orm" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/documentation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/http-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/hydra" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/json-schema" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/jsonld" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/metadata" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/openapi" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/serializer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/state" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/symfony" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/api-platform/validator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/ndjson-react" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/common" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/event-manager" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/instantiator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/migrations" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/orm" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/persistence" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/sql-formatter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/evenement/evenement" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/fidry/cpu-core-counter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nelmio/cors-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/link" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/react/cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/react/child-process" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/react/dns" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/react/event-loop" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/react/promise" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/react/socket" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/react/stream" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/config" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dependency-injection" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/doctrine-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dotenv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/expression-language" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/filesystem" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/flex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/framework-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php85" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-core" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-csrf" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-http" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/serializer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/type-info" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/uid" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/validator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-link" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/willdurand/negotiation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/staabm/side-effects-detector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/browser-kit" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/dompdf" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/php-font-lib" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/dompdf/php-svg-lib" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/thecodingmachine/safe" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/lcobucci/jwt" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/malio/ednotif-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
|
||||
<excludePattern pattern="reference.php" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/laravel-idea.xml
generated
Normal file
8
.idea/laravel-idea.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="InertiaPackage">
|
||||
<option name="directoryPaths">
|
||||
<list />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/material_theme_project_new.xml
generated
Normal file
12
.idea/material_theme_project_new.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="migrated" value="true" />
|
||||
<option name="pristineConfig" value="false" />
|
||||
<option name="userId" value="-70fca0d0:19b8da49b68:-7ffe" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ferme.iml" filepath="$PROJECT_DIR$/.idea/ferme.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
201
.idea/php.xml
generated
Normal file
201
.idea/php.xml
generated
Normal file
@@ -0,0 +1,201 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MessDetectorOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCSFixerOptionsConfiguration">
|
||||
<option name="codingStandard" value="Custom" />
|
||||
<option name="rulesetPath" value="$PROJECT_DIR$/.php-cs-fixer.dist.php" />
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||
<option name="highlightLevel" value="WARNING" />
|
||||
<option name="transferred" value="true" />
|
||||
</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">
|
||||
<include_path>
|
||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
||||
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
|
||||
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
||||
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/child-process" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/nelmio/cors-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
||||
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
||||
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/common" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
||||
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
|
||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/serializer" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
||||
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||
<path value="$PROJECT_DIR$/vendor/malio/ednotif-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="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">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpUnit">
|
||||
<phpunit_settings>
|
||||
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
|
||||
</phpunit_settings>
|
||||
</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">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/symfony2.xml
generated
Normal file
6
.idea/symfony2.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Symfony2PluginSettings">
|
||||
<option name="pluginEnabled" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
814
.idea/workspace.xml
generated
Normal file
814
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,814 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||
<change afterPath="$PROJECT_DIR$/frontend/components/shipment/shipment-form.vue" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/pages/shipment/[[id]].vue" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/bovin-shipment.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/customer.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/bovin-shipment-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/customer-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/shipment-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/shipment-type-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/shipment-type.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/shipment.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/stores/shipment.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/constants/steps.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/constants/steps.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/index.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/services/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/Address.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Address.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/BovinShipment.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/BovinShipment.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/Shipment.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Shipment.php" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ComposerSettings" synchronizationState="SYNCHRONIZE">
|
||||
<pharConfigPath>$PROJECT_DIR$/composer.json</pharConfigPath>
|
||||
<execution />
|
||||
</component>
|
||||
<component name="CopilotPersistence">
|
||||
<persistenceIdMap>
|
||||
<entry key="_//wsl.localhost/Ubuntu-24.04/home/kevin/Stage/Ferme" value="381AhnCm9yPeOiWgMObKHhtgv2C" />
|
||||
</persistenceIdMap>
|
||||
</component>
|
||||
<component name="EmbeddingIndexingInfo">
|
||||
<option name="cachedIndexableFilesCount" value="151" />
|
||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||
</component>
|
||||
<component name="FileTemplateManagerImpl">
|
||||
<option name="RECENT_TEMPLATES">
|
||||
<list>
|
||||
<option value="Vue Composition API Component" />
|
||||
<option value="TypeScript File" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="fix/makefile" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="McpProjectServerCommands">
|
||||
<commands />
|
||||
<urls />
|
||||
</component>
|
||||
<component name="PhpServers">
|
||||
<servers>
|
||||
<server host="localhost" id="36c0c232-9151-4654-a36c-e0f5fd99da91" name="ferme-docker" port="8080" use_path_mappings="true">
|
||||
<path_mappings>
|
||||
<mapping local-root="$PROJECT_DIR$" remote-root="/var/www/html" />
|
||||
</path_mappings>
|
||||
</server>
|
||||
</servers>
|
||||
</component>
|
||||
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="C:/php-8.4.3/php.exe">
|
||||
<include_path>
|
||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
||||
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
|
||||
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/dns" />
|
||||
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/socket" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/child-process" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/promise" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/react/stream" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/nelmio/cors-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
||||
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
||||
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
||||
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php85" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-uuid" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/common" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
|
||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
||||
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/willdurand/negotiation" />
|
||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/http-cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-common" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/documentation" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/metadata" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/doctrine-orm" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/hydra" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/symfony" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/jsonld" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/serializer" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/json-schema" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/openapi" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/validator" />
|
||||
<path value="$PROJECT_DIR$/vendor/api-platform/state" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
||||
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||
<path value="$PROJECT_DIR$/vendor/malio/ednotif-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 5
|
||||
}</component>
|
||||
<component name="ProjectId" id="381AhnCm9yPeOiWgMObKHhtgv2C" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="autoscrollFromSource" value="true" />
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"git-widget-placeholder": "feat/271-expedition-etape-1",
|
||||
"last_opened_file_path": "/home/sroy/Documents/test/Ferme",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "configurable.tailwindcss",
|
||||
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
],
|
||||
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||
"TEXT"
|
||||
],
|
||||
"vue.recent.templates": [
|
||||
"Vue Composition API Component"
|
||||
]
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||
<recent name="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="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.30387.85" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="" />
|
||||
<created>1767956826164</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1767956826164</updated>
|
||||
<workItem from="1767956827666" duration="7866000" />
|
||||
<workItem from="1768201706520" duration="13383000" />
|
||||
<workItem from="1768287908317" duration="28058000" />
|
||||
<workItem from="1768374298711" duration="12403000" />
|
||||
<workItem from="1768460547451" duration="26946000" />
|
||||
<workItem from="1768547023783" duration="11371000" />
|
||||
<workItem from="1768894030675" duration="83922000" />
|
||||
<workItem from="1769413136483" duration="58000" />
|
||||
<workItem from="1769413279223" duration="40490000" />
|
||||
<workItem from="1769612160652" duration="23952000" />
|
||||
<workItem from="1769696465294" duration="8573000" />
|
||||
<workItem from="1769756623432" duration="21592000" />
|
||||
<workItem from="1770015653091" duration="73000" />
|
||||
<workItem from="1770040138216" duration="6492000" />
|
||||
<workItem from="1770050834470" duration="1873000" />
|
||||
<workItem from="1770054381680" duration="1292000" />
|
||||
<workItem from="1770055690365" duration="370000" />
|
||||
<workItem from="1770056515646" duration="21000" />
|
||||
<workItem from="1770102495553" duration="2280000" />
|
||||
<workItem from="1770195604082" duration="90000" />
|
||||
<workItem from="1770195718952" duration="215000" />
|
||||
<workItem from="1770195959162" duration="18915000" />
|
||||
<workItem from="1770274844804" duration="3940000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768237763998</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768237763998</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768316052474</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768316052474</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768316835575</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768316835575</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00004" summary="feat : update du fichier AGENTS.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768316965511</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768316965511</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768317786187</created>
|
||||
<option name="number" value="00005" />
|
||||
<option name="presentableId" value="LOCAL-00005" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768317786187</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768318875533</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768318875533</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768318921478</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768318921478</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00008" summary="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768498751836</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768498751836</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00009" summary="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768555180530</created>
|
||||
<option name="number" value="00009" />
|
||||
<option name="presentableId" value="LOCAL-00009" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768555180530</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768832208350</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768832208350</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00011" summary="feat : update du CHANGELOG.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768832516587</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768832516587</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00012" summary="fix : correction de l'accès au swagger en mode dev qui n'était plus accessible">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768940104944</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768940104944</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="feat : ajout de la conf pour le déploiement en recette">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769005220331</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769005220331</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00014" summary="fix : fix de la conf pour le déploiement en recette">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769008700008</created>
|
||||
<option name="number" value="00014" />
|
||||
<option name="presentableId" value="LOCAL-00014" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769008700008</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00015" summary="fix : fix de la conf pour le déploiement en recette">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769014602062</created>
|
||||
<option name="number" value="00015" />
|
||||
<option name="presentableId" value="LOCAL-00015" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769014602062</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00016" summary="fix : migration apache vers nginx pour un déploiement plus simple">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769019284586</created>
|
||||
<option name="number" value="00016" />
|
||||
<option name="presentableId" value="LOCAL-00016" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769019284586</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00017" summary="fix : dernière modification pour le déploiement en recette et le changement de conf vers nginx">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769021756823</created>
|
||||
<option name="number" value="00017" />
|
||||
<option name="presentableId" value="LOCAL-00017" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769021756823</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00018" summary="ci : auto tag + release artefact">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769021818384</created>
|
||||
<option name="number" value="00018" />
|
||||
<option name="presentableId" value="LOCAL-00018" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769021818384</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00019" summary="ci : fix release artefact">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769022071620</created>
|
||||
<option name="number" value="00019" />
|
||||
<option name="presentableId" value="LOCAL-00019" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769022071620</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00020" summary="ci : fix release artefact">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769024603812</created>
|
||||
<option name="number" value="00020" />
|
||||
<option name="presentableId" value="LOCAL-00020" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769024603812</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00021" summary="ci : ajout du script et de la doc déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769026716634</created>
|
||||
<option name="number" value="00021" />
|
||||
<option name="presentableId" value="LOCAL-00021" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769026716634</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00022" summary="fix : correction du path URI pour la création d'un poids dans une réception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769073690382</created>
|
||||
<option name="number" value="00022" />
|
||||
<option name="presentableId" value="LOCAL-00022" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769073690382</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00023" summary="feat : Ajout du bundle Monolog pour la gestion des logs">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769075990984</created>
|
||||
<option name="number" value="00023" />
|
||||
<option name="presentableId" value="LOCAL-00023" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769075990984</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00024" summary="fix : affiche plus détail dans les logs en recette/prod">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769077633390</created>
|
||||
<option name="number" value="00024" />
|
||||
<option name="presentableId" value="LOCAL-00024" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769077633390</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00025" summary="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769079030808</created>
|
||||
<option name="number" value="00025" />
|
||||
<option name="presentableId" value="LOCAL-00025" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769079030808</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00026" summary="fix : doc de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769094376813</created>
|
||||
<option name="number" value="00026" />
|
||||
<option name="presentableId" value="LOCAL-00026" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769094376813</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00027" summary="fix : doc et script de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769096187792</created>
|
||||
<option name="number" value="00027" />
|
||||
<option name="presentableId" value="LOCAL-00027" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769096187792</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00028" summary="fix : doc et script de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769097091268</created>
|
||||
<option name="number" value="00028" />
|
||||
<option name="presentableId" value="LOCAL-00028" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769097091268</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00029" summary="fix : gitea workflow">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769097476629</created>
|
||||
<option name="number" value="00029" />
|
||||
<option name="presentableId" value="LOCAL-00029" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769097476629</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00030" summary="fix : script de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769098182184</created>
|
||||
<option name="number" value="00030" />
|
||||
<option name="presentableId" value="LOCAL-00030" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769098182184</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00031" summary="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769098861988</created>
|
||||
<option name="number" value="00031" />
|
||||
<option name="presentableId" value="LOCAL-00031" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769098861988</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00032" summary="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769100048933</created>
|
||||
<option name="number" value="00032" />
|
||||
<option name="presentableId" value="LOCAL-00032" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769100048933</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00033" summary="feat : ajout de la debug bar en mod dev">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769177611987</created>
|
||||
<option name="number" value="00033" />
|
||||
<option name="presentableId" value="LOCAL-00033" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769177611987</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00034" summary="feat : ajout du bundle Malio ednotif pour l'utilisation des WS">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769184861047</created>
|
||||
<option name="number" value="00034" />
|
||||
<option name="presentableId" value="LOCAL-00034" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769184861047</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00035" summary="fix : modification de la conf du bundle ednotif">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769434793487</created>
|
||||
<option name="number" value="00035" />
|
||||
<option name="presentableId" value="LOCAL-00035" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769434793487</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00036" summary="feat : update du CHANGELOG.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769435038236</created>
|
||||
<option name="number" value="00036" />
|
||||
<option name="presentableId" value="LOCAL-00036" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769435038236</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00037" summary="feat : finalisation de l'étape 1 "Réception" (formulaire)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769529522614</created>
|
||||
<option name="number" value="00037" />
|
||||
<option name="presentableId" value="LOCAL-00037" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769529522614</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00038" summary="feat : ajout du numéro identification des receptions et ajustement du bon de reception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769676223697</created>
|
||||
<option name="number" value="00038" />
|
||||
<option name="presentableId" value="LOCAL-00038" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769676223697</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00039" summary="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769700808988</created>
|
||||
<option name="number" value="00039" />
|
||||
<option name="presentableId" value="LOCAL-00039" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769700808988</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00040" summary="feat : mise en place de composant UI pour les select, checkbox, date, text">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769705141157</created>
|
||||
<option name="number" value="00040" />
|
||||
<option name="presentableId" value="LOCAL-00040" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769705141157</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00041" summary="feat : update CHANGELOG.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769705240487</created>
|
||||
<option name="number" value="00041" />
|
||||
<option name="presentableId" value="LOCAL-00041" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769705240487</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00042" summary="feat : ajout de commentaire">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769760766200</created>
|
||||
<option name="number" value="00042" />
|
||||
<option name="presentableId" value="LOCAL-00042" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769760766200</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00043" summary="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769768517942</created>
|
||||
<option name="number" value="00043" />
|
||||
<option name="presentableId" value="LOCAL-00043" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769768517943</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00044" summary="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769770092190</created>
|
||||
<option name="number" value="00044" />
|
||||
<option name="presentableId" value="LOCAL-00044" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769770092190</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00045" summary="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769770142624</created>
|
||||
<option name="number" value="00045" />
|
||||
<option name="presentableId" value="LOCAL-00045" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769770142624</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00046" summary="feat : mise à jour du bon de réception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769782099473</created>
|
||||
<option name="number" value="00046" />
|
||||
<option name="presentableId" value="LOCAL-00046" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769782099473</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00047" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770131226364</created>
|
||||
<option name="number" value="00047" />
|
||||
<option name="presentableId" value="LOCAL-00047" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770131226364</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00048" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770206668867</created>
|
||||
<option name="number" value="00048" />
|
||||
<option name="presentableId" value="LOCAL-00048" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770206668867</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00049" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770217875423</created>
|
||||
<option name="number" value="00049" />
|
||||
<option name="presentableId" value="LOCAL-00049" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770217875423</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="50" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="RECENT_FILTERS">
|
||||
<map>
|
||||
<entry key="Branch">
|
||||
<value>
|
||||
<list>
|
||||
<RecentGroup>
|
||||
<option name="FILTER_VALUES">
|
||||
<option value="HEAD" />
|
||||
</option>
|
||||
</RecentGroup>
|
||||
<RecentGroup>
|
||||
<option name="FILTER_VALUES">
|
||||
<option value="develop" />
|
||||
</option>
|
||||
</RecentGroup>
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State>
|
||||
<option name="FILTERS">
|
||||
<map>
|
||||
<entry key="branch">
|
||||
<value>
|
||||
<list>
|
||||
<option value="HEAD" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</State>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="fix : correction du path URI pour la création d'un poids dans une réception" />
|
||||
<MESSAGE value="feat : Ajout du bundle Monolog pour la gestion des logs" />
|
||||
<MESSAGE value="fix : affiche plus détail dans les logs en recette/prod" />
|
||||
<MESSAGE value="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod" />
|
||||
<MESSAGE value="fix : doc de déploiement" />
|
||||
<MESSAGE value="fix : doc et script de déploiement" />
|
||||
<MESSAGE value="fix : gitea workflow" />
|
||||
<MESSAGE value="fix : script de déploiement" />
|
||||
<MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" />
|
||||
<MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" />
|
||||
<MESSAGE value="feat : ajout de la debug bar en mod dev" />
|
||||
<MESSAGE value="feat : ajout du bundle Malio ednotif pour l'utilisation des WS" />
|
||||
<MESSAGE value="fix : modification de la conf du bundle ednotif" />
|
||||
<MESSAGE value="feat : update du CHANGELOG.md" />
|
||||
<MESSAGE value="feat : finalisation de l'étape 1 "Réception" (formulaire)" />
|
||||
<MESSAGE value="feat : ajout du numéro identification des receptions et ajustement du bon de reception" />
|
||||
<MESSAGE value="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception" />
|
||||
<MESSAGE value="feat : mise en place de composant UI pour les select, checkbox, date, text" />
|
||||
<MESSAGE value="feat : update CHANGELOG.md" />
|
||||
<MESSAGE value="feat : ajout de commentaire" />
|
||||
<MESSAGE value="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception" />
|
||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception" />
|
||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||
</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="javascript">
|
||||
<url>file://$PROJECT_DIR$/frontend/services/shipment.ts</url>
|
||||
<properties lambdaOrdinal="-1" />
|
||||
<option name="timeStamp" value="37" />
|
||||
</line-breakpoint>
|
||||
</breakpoints>
|
||||
</breakpoint-manager>
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
<select />
|
||||
</component>
|
||||
<component name="github-copilot-workspace">
|
||||
<instructionFileLocations>
|
||||
<option value=".github/instructions" />
|
||||
</instructionFileLocations>
|
||||
<promptFileLocations>
|
||||
<option value=".github/prompts" />
|
||||
</promptFileLocations>
|
||||
</component>
|
||||
</project>
|
||||
56
.php-cs-fixer.dist.php
Normal file
56
.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Finder;
|
||||
|
||||
$finder = Finder::create()
|
||||
->in('src')
|
||||
->notName('Kernel.php')
|
||||
;
|
||||
|
||||
$rules = [
|
||||
'@Symfony' => true,
|
||||
'@PSR12' => true,
|
||||
'@PHP84Migration' => true,
|
||||
'@PER-CS' => true,
|
||||
'@PhpCsFixer' => true,
|
||||
'strict_param' => true,
|
||||
'strict_comparison' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_useless_return' => true,
|
||||
'binary_operator_spaces' => [
|
||||
'operators' => [
|
||||
'=' => 'align_single_space_minimal',
|
||||
'||' => 'align_single_space_minimal',
|
||||
'=>' => 'align_single_space_minimal',
|
||||
],
|
||||
],
|
||||
'global_namespace_import' => [
|
||||
'import_classes' => true,
|
||||
'import_constants' => true,
|
||||
'import_functions' => true,
|
||||
],
|
||||
'modernize_strpos' => true, // needs PHP 8+ or polyfill
|
||||
'no_superfluous_phpdoc_tags' => true,
|
||||
'echo_tag_syntax' => true,
|
||||
'semicolon_after_instruction' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'ternary_to_null_coalescing' => true,
|
||||
'declare_strict_types' => true,
|
||||
'operator_linebreak' => [
|
||||
'position' => 'beginning',
|
||||
],
|
||||
'no_unused_imports' => true,
|
||||
'single_line_throw' => false,
|
||||
'php_unit_test_class_requires_covers' => false,
|
||||
];
|
||||
|
||||
$config = new Config();
|
||||
|
||||
return $config
|
||||
->setRiskyAllowed(true)
|
||||
->setRules($rules)
|
||||
->setFinder($finder)
|
||||
;
|
||||
59
AGENTS.md
Normal file
59
AGENTS.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 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).
|
||||
- Reception also has `identification_number` (auto `N-BR-####`), `merchandise_type`, `merchandise_detail`, `buildings` (M2M), and `pellet_buildings` (via `reception_pellet_building`).
|
||||
- `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`).
|
||||
- Global font stack uses Helvetica via Tailwind (`font-sans`) and `frontend/assets/css/main.css`.
|
||||
- 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`.
|
||||
- Step 2 uses `frontend/components/reception/reception-product-received.vue` for merchandise selection; type codes in `frontend/utils/constants.ts`.
|
||||
- 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).
|
||||
- New reference data added:
|
||||
- Reception types (`reception_type`, fields: `label`, `code`), selectable on reception form.
|
||||
- Merchandise types (`merchandise_type`, fields: `label`, `code`) and pellet types (`pellet_type`, fields: `label`, `code`).
|
||||
- Buildings (`building`, fields: `label`, `code`) and reception allocations (`reception_building` M2M, `reception_pellet_building` unique on reception/pellet/building).
|
||||
- Suppliers (`supplier`) with addresses (`address`, fields: `label`, `street`, `postal_code`, `city`, `country_code` ISO2), via `supplier_address` join table.
|
||||
- Trucks (`truck`, field: `name`), linked to receptions.
|
||||
- Carriers (`carrier`, fields: `name`, nullable `code`), Drivers (`driver`, fields: `name`, `carrier_id`), Vehicles (`vehicle`, fields: `plate`, `carrier_id`, `truck_id`) used for LIOT logic.
|
||||
- Reception links: `reception_type_id`, `supplier_id`, `address_id`, `truck_id`, `carrier_id`, `driver_id`, `user_id`.
|
||||
- Address exposes `fullAddress` via getter for display.
|
||||
- LIOT behavior in reception form: if carrier code = `LIOT`, show driver + vehicle selects and hide manual license plate input; vehicle list filters by truck type and carrier; selected vehicle sets `license_plate`.
|
||||
47
CHANGELOG.md
Normal file
47
CHANGELOG.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
Liste des évolutions du projet Ferme
|
||||
|
||||
## [0.0.0]
|
||||
### Parameters
|
||||
Ajouter dans le fichier .env
|
||||
- DEFAULT_URI
|
||||
- DATABASE_URL
|
||||
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||
- PONT_BASCULE_URL
|
||||
- JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
||||
- JWT_PUBLIC_KEY
|
||||
- JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||
- COOKIE_SECURE=0 (en dev 0 et en prod 1)
|
||||
- EDNOTIF_EXPLOITATION_CODE
|
||||
- EDNOTIF_EXPLOITATION_NUMERO
|
||||
- EDNOTIF_LOGIN
|
||||
- EDNOTIF_PASSWORD
|
||||
|
||||
Ajouter dans le fichier .env du frontend
|
||||
- NUXT_PUBLIC_API_BASE
|
||||
|
||||
### Added
|
||||
* [#203] Réceptions — Parcours de pesée multi-étapes (début)
|
||||
* [#202] Authentification — Connexion utilisateur (JWT)
|
||||
* Ajout du bundle malio/ednotif-bundle
|
||||
* Ajout de composant UI
|
||||
* Finalisation de la partie réception de marchandise
|
||||
* [#267] Lister les réceptions en attente
|
||||
* [#268] Lister les réceptions terminées
|
||||
* [#316] Admin liste des transporteurs
|
||||
* [#312] Creation administration listing fournisseurs
|
||||
* [#315] Creation page admin utilisateur
|
||||
* [#317] Admin modification creation transporteur
|
||||
* [#318] Affichage modification reception terminée
|
||||
* [#320] Affichage modification reception terminée suite
|
||||
* [#271] Créer une nouvelle expédition (étape 1)
|
||||
* [#272] Créer une nouvelle expédition (étape 2)
|
||||
* [#273] Créer une nouvelle expédition (étape 3)
|
||||
* [#256] Créer une nouvelle réception (étape 3 - bovin)
|
||||
* [#314] Création d'une page d'administration : listing des utilisateurs
|
||||
* [#276] Lister les expéditions terminées
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
90
DEPLOYMENT.md
Normal file
90
DEPLOYMENT.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Déploiement Ferme (release Gitea)
|
||||
|
||||
## 1) Premier déploiement
|
||||
|
||||
### Pré-requis système (Ubuntu)
|
||||
1. Mettre à jour la machine
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y software-properties-common ca-certificates curl gnupg unzip git nginx
|
||||
```
|
||||
2. Installer PHP 8.4 + FPM + extensions
|
||||
```bash
|
||||
sudo add-apt-repository -y ppa:ondrej/php
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
php8.4 php8.4-fpm php8.4-cli php8.4-common \
|
||||
php8.4-mbstring php8.4-xml php8.4-curl php8.4-intl \
|
||||
php8.4-zip php8.4-gd php8.4-pgsql php8.4-opcache
|
||||
```
|
||||
3. Installer PostgreSQL (si la DB est locale)
|
||||
```bash
|
||||
sudo apt install -y postgresql postgresql-contrib
|
||||
sudo -u postgres psql
|
||||
```
|
||||
Dans psql :
|
||||
```sql
|
||||
CREATE USER ferme_user WITH PASSWORD 'motdepassefort';
|
||||
CREATE DATABASE ferme OWNER ferme_user;
|
||||
\q
|
||||
```
|
||||
|
||||
### Dossier de déploiement
|
||||
1. Créer le dossier de déploiement
|
||||
```bash
|
||||
sudo mkdir -p /var/www/ferme
|
||||
sudo chown -R malio:malio /var/www/ferme
|
||||
```
|
||||
2. Créer le fichier d’environnement
|
||||
- Backend : `/var/www/ferme/.env`
|
||||
- `APP_ENV=prod`
|
||||
- `APP_DEBUG=0`
|
||||
- `APP_SECRET=...`
|
||||
- `DATABASE_URL=postgresql://ferme_user:motdepassefort@127.0.0.1:5432/ferme?serverVersion=16&charset=utf8`
|
||||
- `JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem`
|
||||
- `JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem`
|
||||
- `JWT_PASSPHRASE=...`
|
||||
- `COOKIE_SECURE=1`
|
||||
- `PONT_BASCULE_BYPASS=false`
|
||||
3. Générer les clés JWT
|
||||
```bash
|
||||
cd /var/www/ferme
|
||||
mkdir -p config/jwt
|
||||
php bin/console lexik:jwt:generate-keypair
|
||||
```
|
||||
4. Config Nginx (sous-domaine)<br>
|
||||
Copier le fichier de conf /deploy/nginx/ferme.conf dans /etc/nginx/sites-available/ferme.conf
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/ferme.conf /etc/nginx/sites-enabled/ferme.conf
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
5. Installer le script de déploiement (disponible /scripts/deploy-release.sh)
|
||||
```bash
|
||||
sudo nano /usr/local/bin/deploy-ferme
|
||||
sudo chmod +x /usr/local/bin/deploy-ferme
|
||||
```
|
||||
|
||||
## 2) Déployer une release
|
||||
|
||||
1. Créer un tag sur `develop` (auto-tag `v0.0.X`)
|
||||
2. Attendre que la release Gitea soit publiée
|
||||
3. (Une seule fois) Donner les droits d'écriture à PHP sur `var/` via ACL
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y acl
|
||||
sudo setfacl -R -m u:malio:rwx,g:www-data:rwx /var/www/ferme/var
|
||||
sudo setfacl -R -m d:u:malio:rwx,d:g:www-data:rwx /var/www/ferme/var
|
||||
```
|
||||
4. Déployer la release
|
||||
```bash
|
||||
/usr/local/bin/deploy-ferme vX.Y.Z
|
||||
```
|
||||
Notes :
|
||||
- Lancer le déploiement en tant que `malio` (ou `sudo -u malio`) pour éviter de casser les droits.
|
||||
- Le script applique `umask 002` pour garder les fichiers group-writable (`www-data`).
|
||||
- Le script force des droits d'exécution minimaux sur `/var/www` et `/var/www/ferme` pour éviter un blocage Nginx.
|
||||
|
||||
### Vérifications
|
||||
- Front : `http://ferme.malio-dev.fr/`
|
||||
- API : `http://ferme.malio-dev.fr/api/users`
|
||||
- Login : `POST http://ferme.malio-dev.fr/api/login_check`
|
||||
110
README.md
110
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
## Installation du projet
|
||||
### Windows
|
||||
Pour windows, il faut installer le WSL2, Ubuntu 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)
|
||||
|
||||
### Linux
|
||||
@@ -15,12 +15,120 @@ Une fois les prérequis installés, il suffit de cloner le projet et de lancer l
|
||||
make start
|
||||
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.
|
||||
|
||||
### Configuration global
|
||||
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
||||
|
||||
Vérifier que dans le .env.local, vous avez :
|
||||
* APP_SECRET (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));" et doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
||||
* DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
||||
* PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||
* PONT_BASCULE_URL
|
||||
* JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
||||
* JWT_PUBLIC_KEY
|
||||
* JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
||||
* COOKIE_SECURE=0 (en dev 0 et en prod 1. Si c'est du http, laisser en 0)
|
||||
|
||||
Vérifier que dans le .env du dossier frontend, vous avez :
|
||||
* NUXT_PUBLIC_API_BASE="http://localhost:8080/api"
|
||||
|
||||
### Configuration xdebug
|
||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||
Pour cela, il faut aller dans **Settings > PHP > Servers** <br>
|
||||
* Name : ferme-docker
|
||||
* Host : localhost
|
||||
* Port : 8080
|
||||
* Path : File/Directory -> l'endroit où est stocké votre projet et le path -> /var/www/html
|
||||
|
||||
Pour que xdebug fonctionne sur windows, il faut modifier la variable **XDEBUG_CLIENT_HOST** par votre ip local
|
||||
|
||||
## Utilisation du projet
|
||||
### Backend
|
||||
L'api est disponible sur http://localhost:8080/api
|
||||
Pour la bdd toutes les infos sont dans le fichier **docker/.env.docker.local**
|
||||
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.
|
||||
C'est un bdd local dans le docker.
|
||||
### Frontend
|
||||
Pour le frontend, il suffit de taper la commande suivante qui va lancer le serveur de dev
|
||||
```bash
|
||||
make dev-nuxt
|
||||
```
|
||||
Le front sera accessible sur http://localhost:3000
|
||||
|
||||
### Authentification
|
||||
Ce projet utilise l'authentification JWT avec un cookie httpOnly (LexikJWTAuthenticationBundle).
|
||||
Le frontend ne lit jamais directement le token, le navigateur envoie automatiquement le cookie.
|
||||
|
||||
### Login flow
|
||||
- Frontend envoie les identifiants à:
|
||||
- `POST /api/login_check`
|
||||
- Backend returns:
|
||||
- `204 No Content` (normal)
|
||||
- `Set-Cookie: BEARER=...; HttpOnly`
|
||||
- Le cookie est automatiquement envoyé pour les futures requêtes.
|
||||
- La déconnexion utilise `POST /api/logout` et redirige vers `/login`.
|
||||
|
||||
### Fixtures
|
||||
Pour lancer les fixtures (Attention sa purge la bdd complètement)
|
||||
```bash
|
||||
php bin/console doctrine:fixtures:load
|
||||
```
|
||||
|
||||
Attention cette commande est dangereuse, à utiliser que pour les débuts de la prod ou en recette.
|
||||
Dans un premier temps pour remplir les listes, vous pouvez lancer la commande symfony
|
||||
```bash
|
||||
php bin/console app:seed
|
||||
```
|
||||
La commande va faire une update ou une création en fonction des data existante.
|
||||
|
||||
## Livraison en recette
|
||||
### Préparatifs
|
||||
Avant de déployer, il faut penser à ajouter les variables d'env s'il y a des changements/modifications.
|
||||
Le .env se trouve /var/www/ferme/.env
|
||||
|
||||
Le script de livraison est version dans le repo dans script/deploy-release.sh <br>
|
||||
Sur la machine, il est disponible dans /usr/local/bin/deploy-ferme <br>
|
||||
Pour le modifier, il faut copier le contenu du deploy-release.sh dans le deploy-ferme
|
||||
### Livraison
|
||||
Sur le serveur de recette, il suffit d'utiliser cette commande pour livrer
|
||||
```bash
|
||||
/usr/local/bin/deploy-ferme vX.Y.Z
|
||||
```
|
||||
## Commandes utiles
|
||||
Pour restart le container
|
||||
```bash
|
||||
make restart
|
||||
```
|
||||
Pour lancer les TU
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
Pour accéder au container et lance des commandes
|
||||
```bash
|
||||
make shell
|
||||
```
|
||||
Pour clear le cache Symfony
|
||||
```bash
|
||||
make cache-clear
|
||||
```
|
||||
Faire une migration
|
||||
```bash
|
||||
make migration-migrate
|
||||
```
|
||||
Pour générer un password pour un user
|
||||
```bash
|
||||
make shell
|
||||
php bin/console security:hash-password
|
||||
```
|
||||
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
||||
```sql
|
||||
INSERT INTO "user" (username, roles, password)
|
||||
VALUES ('Mon user', '["ROLE_USER"]', 'Mon mdp hashé');
|
||||
```
|
||||
## Gestion des logs
|
||||
Pour suivre les logs en temps réel :
|
||||
* tail -f var/log/dev.log
|
||||
* tail -f var/log/prod.log
|
||||
|
||||
21
bin/console
Executable file
21
bin/console
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||
}
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
||||
4
bin/phpunit
Executable file
4
bin/phpunit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
31
commit-msg
Normal file
31
commit-msg
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MSG_FILE="${1}"
|
||||
FIRST_LINE="$(head -n 1 "$MSG_FILE" | tr -d '\r')"
|
||||
|
||||
# Autoriser commits auto-générés par git
|
||||
if [[ "$FIRST_LINE" =~ ^Merge\ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Types autorisés (MINUSCULES uniquement)
|
||||
# Optionnel: scope => feat(auth) : ...
|
||||
REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+'
|
||||
|
||||
if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then
|
||||
echo "❌ Message de commit invalide."
|
||||
echo ""
|
||||
echo "➡️ Format attendu : <type>(<scope optionnel>) : <message>"
|
||||
echo "➡️ Types autorisés (minuscules uniquement) :"
|
||||
echo " build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
|
||||
echo ""
|
||||
echo "✅ Exemples :"
|
||||
echo " feat : add login page"
|
||||
echo " fix(auth) : prevent null token crash"
|
||||
echo " docs : update README"
|
||||
echo ""
|
||||
echo "❌ Exemple refusé :"
|
||||
echo " Feat : add login page"
|
||||
exit 1
|
||||
fi
|
||||
106
composer.json
Normal file
106
composer.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"api-platform/doctrine-orm": "^4.2",
|
||||
"api-platform/symfony": "^4.2",
|
||||
"doctrine/doctrine-bundle": "^3.2",
|
||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||
"doctrine/orm": "^3.6",
|
||||
"dompdf/dompdf": "^3.1",
|
||||
"lexik/jwt-authentication-bundle": "*",
|
||||
"malio/ednotif-bundle": ">=0.0.4",
|
||||
"nelmio/cors-bundle": "^2.6",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"symfony/asset": "8.0.*",
|
||||
"symfony/console": "8.0.*",
|
||||
"symfony/dotenv": "8.0.*",
|
||||
"symfony/expression-language": "8.0.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "8.0.*",
|
||||
"symfony/http-client": "8.0.*",
|
||||
"symfony/monolog-bundle": "^4.0",
|
||||
"symfony/property-access": "8.0.*",
|
||||
"symfony/property-info": "8.0.*",
|
||||
"symfony/runtime": "8.0.*",
|
||||
"symfony/security-bundle": "8.0.*",
|
||||
"symfony/serializer": "8.0.*",
|
||||
"symfony/twig-bundle": "8.0.*",
|
||||
"symfony/validator": "8.0.*",
|
||||
"symfony/yaml": "8.0.*"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
"bump-after-update": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php73": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*",
|
||||
"symfony/polyfill-php83": "*",
|
||||
"symfony/polyfill-php84": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "8.0.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.3",
|
||||
"friendsofphp/php-cs-fixer": "^3.92",
|
||||
"phpunit/phpunit": "^12.5",
|
||||
"symfony/browser-kit": "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"
|
||||
}
|
||||
]
|
||||
}
|
||||
11671
composer.lock
generated
Normal file
11671
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
config/bundles.php
Normal file
33
config/bundles.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
||||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
||||
use Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle;
|
||||
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
||||
use Malio\EdnotifBundle\EdnotifBundle;
|
||||
use Nelmio\CorsBundle\NelmioCorsBundle;
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\MakerBundle\MakerBundle;
|
||||
use Symfony\Bundle\MonologBundle\MonologBundle;
|
||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
|
||||
|
||||
return [
|
||||
FrameworkBundle::class => ['all' => true],
|
||||
TwigBundle::class => ['all' => true],
|
||||
SecurityBundle::class => ['all' => true],
|
||||
DoctrineBundle::class => ['all' => true],
|
||||
DoctrineMigrationsBundle::class => ['all' => true],
|
||||
NelmioCorsBundle::class => ['all' => true],
|
||||
LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||
ApiPlatformBundle::class => ['all' => true],
|
||||
MonologBundle::class => ['all' => true],
|
||||
EdnotifBundle::class => ['all' => true],
|
||||
WebProfilerBundle::class => ['dev' => true],
|
||||
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
MakerBundle::class => ['dev' => true],
|
||||
];
|
||||
12
config/packages/api_platform.yaml
Normal file
12
config/packages/api_platform.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
api_platform:
|
||||
title: Hello API Platform
|
||||
version: 1.0.0
|
||||
defaults:
|
||||
stateless: true
|
||||
cache_headers:
|
||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||
formats:
|
||||
json: ['application/json']
|
||||
jsonld: ['application/ld+json']
|
||||
patch_formats:
|
||||
json: ['application/merge-patch+json']
|
||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
#prefix_seed: your_vendor_name/app_name
|
||||
|
||||
# The "app" cache stores to the filesystem by default.
|
||||
# The data in this cache should persist between deploys.
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
# Namespaced pools use the above "app" backend by default
|
||||
#pools:
|
||||
#my.dedicated.cache: null
|
||||
48
config/packages/doctrine.yaml
Normal file
48
config/packages/doctrine.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '16'
|
||||
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
orm:
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
identity_generation_preferences:
|
||||
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
controller_resolver:
|
||||
auto_mapping: false
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
6
config/packages/doctrine_migrations.yaml
Normal file
6
config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
13
config/packages/ednotif.yaml
Normal file
13
config/packages/ednotif.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
ednotif:
|
||||
guichet_wsdl: 'https://ws-reswel-elv.equade.fr/wsguichet/WsGuichet?wsdl'
|
||||
metier_wsdl: 'https://ws-ednotif.equade.fr/wsIpBNotif/wsIpBNotif?wsdl'
|
||||
exploitation_code: '%env(string:EDNOTIF_EXPLOITATION_CODE)%'
|
||||
exploitation_number: '%env(string:EDNOTIF_EXPLOITATION_NUMERO)%'
|
||||
exploitation_country_code: 'FR'
|
||||
login: '%env(string:EDNOTIF_LOGIN)%'
|
||||
password: '%env(string:EDNOTIF_PASSWORD)%'
|
||||
token_ttl_seconds: 900
|
||||
soap_options:
|
||||
trace: false
|
||||
exceptions: true
|
||||
connection_timeout: 15
|
||||
15
config/packages/framework.yaml
Normal file
15
config/packages/framework.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
|
||||
# Note that the session will be started ONLY if you read or write from it.
|
||||
session: true
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
20
config/packages/lexik_jwt_authentication.yaml
Normal file
20
config/packages/lexik_jwt_authentication.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
lexik_jwt_authentication:
|
||||
secret_key: '%kernel.project_dir%/config/jwt/private.pem'
|
||||
public_key: '%kernel.project_dir%/config/jwt/public.pem'
|
||||
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||
token_ttl: 86400
|
||||
token_extractors:
|
||||
authorization_header:
|
||||
enabled: true
|
||||
prefix: Bearer
|
||||
name: Authorization
|
||||
cookie:
|
||||
enabled: true
|
||||
name: BEARER
|
||||
set_cookies:
|
||||
BEARER:
|
||||
lifetime: 86400
|
||||
path: /
|
||||
samesite: lax
|
||||
secure: '%env(bool:COOKIE_SECURE)%'
|
||||
httpOnly: true
|
||||
28
config/packages/monolog.yaml
Normal file
28
config/packages/monolog.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
monolog:
|
||||
channels: [deprecation]
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!deprecation"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: "%kernel.logs_dir%/deprecations.log"
|
||||
11
config/packages/nelmio_cors.yaml
Normal file
11
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
nelmio_cors:
|
||||
defaults:
|
||||
origin_regex: true
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
allow_credentials: true
|
||||
expose_headers: ['Link']
|
||||
max_age: 3600
|
||||
paths:
|
||||
'^/': null
|
||||
4
config/packages/prod/api_platform.yaml
Normal file
4
config/packages/prod/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
api_platform:
|
||||
enable_docs: false
|
||||
enable_swagger: false
|
||||
enable_swagger_ui: false
|
||||
3
config/packages/property_info.yaml
Normal file
3
config/packages/property_info.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
10
config/packages/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
framework:
|
||||
router:
|
||||
# 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: '%env(DEFAULT_URI)%'
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
70
config/packages/security.yaml
Normal file
70
config/packages/security.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
App\Entity\User: 'auto'
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: username
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
# Ensure dev tools and static assets are always allowed
|
||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||
security: false
|
||||
login:
|
||||
pattern: ^/login_check
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
json_login:
|
||||
check_path: /login_check
|
||||
username_path: username
|
||||
password_path: password
|
||||
success_handler: lexik_jwt_authentication.handler.authentication_success
|
||||
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
||||
api:
|
||||
pattern: ^/
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
jwt: ~
|
||||
logout:
|
||||
path: /api/logout
|
||||
target: /login
|
||||
enable_csrf: false
|
||||
delete_cookies:
|
||||
BEARER:
|
||||
path: /
|
||||
|
||||
# Activate different ways to authenticate:
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# Note: Only the *first* matching rule is applied
|
||||
access_control:
|
||||
# Login JWT
|
||||
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
||||
# Liste des users en lecture publique
|
||||
- { path: ^/api/users, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||
# Doc API (swagger) en public
|
||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||
# Version de l'application en public
|
||||
- { path: ^/api/version, roles: PUBLIC_ACCESS, methods: [GET] }
|
||||
# Tout le reste nécessite un JWT
|
||||
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# Password hashers are resource-intensive by design to ensure security.
|
||||
# In tests, it's safe to reduce their cost to improve performance.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
6
config/packages/twig.yaml
Normal file
6
config/packages/twig.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
11
config/packages/validator.yaml
Normal file
11
config/packages/validator.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
framework:
|
||||
validation:
|
||||
# Enables validator auto-mapping support.
|
||||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
# App\Entity\: []
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
7
config/packages/web_profiler.yaml
Normal file
7
config/packages/web_profiler.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
collect_serializer_data: true
|
||||
5
config/preload.php
Normal file
5
config/preload.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
||||
1943
config/reference.php
Normal file
1943
config/reference.php
Normal file
File diff suppressed because it is too large
Load Diff
11
config/routes.yaml
Normal file
11
config/routes.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
||||
|
||||
# This file is the entry point to configure the routes of your app.
|
||||
# Methods with the #[Route] attribute are automatically imported.
|
||||
# See also https://symfony.com/doc/current/routing.html
|
||||
|
||||
# To list all registered routes, run the following command:
|
||||
# bin/console debug:router
|
||||
|
||||
controllers:
|
||||
resource: routing.controllers
|
||||
4
config/routes/api_platform.yaml
Normal file
4
config/routes/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
api_platform:
|
||||
resource: .
|
||||
type: api_platform
|
||||
prefix: /api
|
||||
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||
prefix: /_error
|
||||
7
config/routes/security.yaml
Normal file
7
config/routes/security.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
|
||||
api_login:
|
||||
path: /login_check
|
||||
methods: [POST]
|
||||
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||
prefix: /_profiler
|
||||
31
config/services.yaml
Normal file
31
config/services.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
||||
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
# See also https://symfony.com/doc/current/service_container/import.html
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
imports:
|
||||
- { resource: version.yaml }
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
|
||||
App\Service\PontBasculeService:
|
||||
arguments:
|
||||
$endpoint: '%env(PONT_BASCULE_URL)%'
|
||||
$bypass: '%env(bool:PONT_BASCULE_BYPASS)%'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
2
config/version.yaml
Normal file
2
config/version.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.0.39'
|
||||
43
deploy/nginx/ferme.conf
Normal file
43
deploy/nginx/ferme.conf
Normal file
@@ -0,0 +1,43 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name ferme.malio-dev.fr;
|
||||
|
||||
root /var/www/ferme/frontend/.output/public;
|
||||
index index.html;
|
||||
|
||||
location ^~ /api/ {
|
||||
root /var/www/ferme/public;
|
||||
try_files $uri /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ^~ /bundles/ {
|
||||
root /var/www/ferme/public;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /api/login_check {
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/ferme/public/index.php;
|
||||
fastcgi_param DOCUMENT_ROOT /var/www/ferme/public;
|
||||
fastcgi_param SCRIPT_NAME /index.php;
|
||||
fastcgi_param PATH_INFO /login_check;
|
||||
fastcgi_param REQUEST_URI /login_check;
|
||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/ferme/public/index.php;
|
||||
fastcgi_param DOCUMENT_ROOT /var/www/ferme/public;
|
||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
||||
internal;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
57
docker-compose.yml
Normal file
57
docker-compose.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
services:
|
||||
php:
|
||||
container_name: php-${DOCKER_APP_NAME}-fpm
|
||||
build:
|
||||
context: ./docker/php
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
DOCKER_PHP_VERSION: ${DOCKER_PHP_VERSION}
|
||||
DOCKER_NODE_VERSION: ${DOCKER_NODE_VERSION}
|
||||
CURRENT_UID: ${CURRENT_UID}
|
||||
CURRENT_GID: ${CURRENT_GID}
|
||||
environment:
|
||||
PHP_IDE_CONFIG: serverName=${DOCKER_APP_NAME}-docker
|
||||
XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal}
|
||||
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"
|
||||
COMPOSER_HOME: /tmp/composer
|
||||
COMPOSER_CACHE_DIR: /tmp/composer/cache
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
- ~/.cache:/var/www/.cache # Pour la cache de composer
|
||||
- ~/.config:/var/www/.config # Pour la config de yarn
|
||||
- ~/.composer:/var/www/.composer # Pour la config de composer
|
||||
- ./docker/php/config/php.ini:/usr/local/etc/php/php.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
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "3000:3000"
|
||||
restart: unless-stopped
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: nginx-${DOCKER_APP_NAME}
|
||||
depends_on:
|
||||
- php
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./:/var/www/html:ro
|
||||
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
pg_data:
|
||||
11
docker/.env.docker
Normal file
11
docker/.env.docker
Normal file
@@ -0,0 +1,11 @@
|
||||
DOCKER_APP_NAME=ferme
|
||||
DOCKER_PHP_VERSION=8.4.6
|
||||
DOCKER_NODE_VERSION=24.12.0
|
||||
APP_USER=www-data
|
||||
POSTGRES_DB=ferme
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
POSTGRES_PORT=5432
|
||||
XDEBUG_CLIENT_HOST=host.docker.internal
|
||||
CURRENT_UID=1004
|
||||
CURRENT_GID=1004
|
||||
52
docker/nginx/conf.d/ferme.conf
Normal file
52
docker/nginx/conf.d/ferme.conf
Normal file
@@ -0,0 +1,52 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /var/www/html/frontend/dist;
|
||||
index index.html;
|
||||
|
||||
location ^~ /api/ {
|
||||
root /var/www/html/public;
|
||||
try_files $uri /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ^~ /_wdt/ {
|
||||
root /var/www/html/public;
|
||||
try_files $uri /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ^~ /_profiler/ {
|
||||
root /var/www/html/public;
|
||||
try_files $uri /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ^~ /bundles/ {
|
||||
root /var/www/html/public;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = /api/login_check {
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||
fastcgi_param SCRIPT_NAME /index.php;
|
||||
fastcgi_param PATH_INFO /login_check;
|
||||
fastcgi_param REQUEST_URI /login_check;
|
||||
fastcgi_pass php:9000;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
||||
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
||||
fastcgi_pass php:9000;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
129
docker/php/Dockerfile
Normal file
129
docker/php/Dockerfile
Normal file
@@ -0,0 +1,129 @@
|
||||
ARG DOCKER_PHP_VERSION
|
||||
|
||||
FROM php:${DOCKER_PHP_VERSION}-fpm-bullseye
|
||||
|
||||
ARG DOCKER_NODE_VERSION
|
||||
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
||||
|
||||
# Installer les dépendances et extensions PHP nécessaires
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libicu-dev \
|
||||
libpq-dev \
|
||||
libpng-dev \
|
||||
libzip-dev \
|
||||
libxml2-dev \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
libbz2-dev \
|
||||
libgmp-dev \
|
||||
libldap2-dev \
|
||||
libonig-dev \
|
||||
libsodium-dev \
|
||||
libxslt1-dev \
|
||||
unixodbc-dev \
|
||||
libsqlite3-dev \
|
||||
zlib1g-dev \
|
||||
libssl-dev \
|
||||
libc-client-dev \
|
||||
libkrb5-dev \
|
||||
freetds-dev \
|
||||
vim \
|
||||
tcpdump \
|
||||
dnsutils \
|
||||
wget \
|
||||
git \
|
||||
unzip \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
intl \
|
||||
zip \
|
||||
bcmath \
|
||||
bz2 \
|
||||
calendar \
|
||||
exif \
|
||||
gd \
|
||||
gettext \
|
||||
gmp \
|
||||
ldap \
|
||||
# mysqli \
|
||||
pcntl \
|
||||
pdo_pgsql \
|
||||
# pdo_mysql \
|
||||
# pdo_sqlite \
|
||||
# pdo_sqlsrv \
|
||||
soap \
|
||||
sockets \
|
||||
sysvsem \
|
||||
xsl
|
||||
|
||||
|
||||
# Installation de node
|
||||
RUN wget -qO- "https://nodejs.org/dist/v${DOCKER_NODE_VERSION}/node-v${DOCKER_NODE_VERSION}-linux-x64.tar.xz" | tar xJC /tmp/ && \
|
||||
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/bin /usr/ && \
|
||||
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/include /usr/ && \
|
||||
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/lib /usr/ && \
|
||||
cp -r /tmp/node-v${DOCKER_NODE_VERSION}-linux-x64/share /usr/ && \
|
||||
npm install --global yarn
|
||||
|
||||
# installation/activation d'extensions php
|
||||
RUN pecl install xdebug
|
||||
RUN docker-php-ext-enable xdebug && \
|
||||
docker-php-ext-install zip && \
|
||||
docker-php-ext-install gd && \
|
||||
docker-php-ext-install soap && \
|
||||
docker-php-ext-configure intl && \
|
||||
docker-php-ext-install intl
|
||||
|
||||
# Configuration spéciale pour quelques extensions
|
||||
# RUN docker-php-ext-configure pdo_odbc --with-pdo-odbc=unixODBC,/usr && \
|
||||
# docker-php-ext-install pdo_odbc \
|
||||
RUN docker-php-ext-enable opcache
|
||||
|
||||
# Configurer Oracle OCI8 (nécessite le SDK Oracle, à installer manuellement ou à lier via les dépendances)
|
||||
#RUN apt-get update && apt-get -y install wget unzip libaio1 && \
|
||||
# wget https://download.oracle.com/otn_software/linux/instantclient/2340000/instantclient-basic-linux.x64-23.4.0.24.05.zip && \
|
||||
# unzip -o instantclient-basic-linux.x64-23.4.0.24.05.zip -d /usr/local && \
|
||||
# wget https://download.oracle.com/otn_software/linux/instantclient/2340000/instantclient-sdk-linux.x64-23.4.0.24.05.zip && \
|
||||
# unzip -o instantclient-sdk-linux.x64-23.4.0.24.05.zip -d /usr/local
|
||||
#
|
||||
#RUN echo 'instantclient,/usr/local/instantclient_23_4' | pecl install oci8-3.4.0 \
|
||||
# && docker-php-ext-enable oci8
|
||||
#
|
||||
#ENV ORACLE_BASE /usr/local/instantclient_23_4
|
||||
#ENV LD_LIBRARY_PATH /usr/local/instantclient_23_4
|
||||
#ENV TNS_ADMIN /usr/local/instantclient_23_4
|
||||
#ENV ORACLE_HOME /usr/local/instantclient_23_4
|
||||
|
||||
|
||||
# Configuration pour utiliser Kerberos avec IMAP (si nécessaire)
|
||||
# RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
|
||||
# && docker-php-ext-install imap
|
||||
|
||||
# installation de composer
|
||||
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
|
||||
|
||||
# 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
|
||||
RUN mkdir /var/www/html/LOG
|
||||
|
||||
###> User ###
|
||||
ARG CURRENT_UID
|
||||
ARG CURRENT_GID
|
||||
# mapping du user host avec www-data
|
||||
RUN usermod -o -u ${CURRENT_UID} www-data && groupmod -o -g ${CURRENT_GID} www-data
|
||||
RUN chown www-data:www-data -R /var/www/*
|
||||
RUN chown www-data:www-data -R /var/www/.*
|
||||
###< User ###
|
||||
|
||||
RUN rm -rf \
|
||||
/var/lib/apt/lists/* \
|
||||
/tmp/* \
|
||||
/var/tmp/*
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
EXPOSE 80
|
||||
9
docker/php/config/docker-php-ext-xdebug.ini
Normal file
9
docker/php/config/docker-php-ext-xdebug.ini
Normal file
@@ -0,0 +1,9 @@
|
||||
zend_extension = /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so
|
||||
xdebug.mode=debug
|
||||
xdebug.idekey=PHPSTORM
|
||||
xdebug.start_with_request=yes
|
||||
xdebug.discover_client_host=1
|
||||
xdebug.client_port=9003
|
||||
xdebug.log="/var/www/html/LOG/xdebug.log"
|
||||
xdebug.log_level=0
|
||||
xdebug.connect_timeout_ms=2
|
||||
4
docker/php/config/php.ini
Normal file
4
docker/php/config/php.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
[Date]
|
||||
; Defines the default timezone used by the date functions
|
||||
; http://php.net/date.timezone
|
||||
date.timezone = Europe/Paris
|
||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
75
frontend/README.md
Normal file
75
frontend/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
13
frontend/app.vue
Normal file
13
frontend/app.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { load } = useAppVersion()
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
9
frontend/assets/css/main.css
Normal file
9
frontend/assets/css/main.css
Normal file
@@ -0,0 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
18
frontend/assets/css/toast.css
Normal file
18
frontend/assets/css/toast.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.iziToast {
|
||||
font-size: 16px;
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.iziToast > .iziToast-body {
|
||||
padding: 18px 24px;
|
||||
}
|
||||
|
||||
.iziToast > .iziToast-body .iziToast-title {
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.iziToast > .iziToast-body .iziToast-message {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
0
frontend/components/.gitkeep
Normal file
0
frontend/components/.gitkeep
Normal file
30
frontend/components/card-link.vue
Normal file
30
frontend/components/card-link.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="link">
|
||||
<div class="w-[324px] h-[228px] border border-black rounded-md p-6 flex flex-col justify-between">
|
||||
<div class="flex justify-between">
|
||||
<div class="rounded-full w-[80px] h-[80px] bg-neutral-400 flex justify-center items-center">
|
||||
<Icon :name="iconName" style="color: black" size="44" />
|
||||
</div>
|
||||
<div>
|
||||
<Icon name="mdi:plus" style="color: black" size="44" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="uppercase font-bold">
|
||||
<p class="text-3xl"> {{ label }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
link: string
|
||||
iconName: string
|
||||
label: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
183
frontend/components/reception/reception-bovine-received.vue
Normal file
183
frontend/components/reception/reception-bovine-received.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
||||
class="flex flex-col items-center gap-16">
|
||||
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1>
|
||||
<div
|
||||
class="flex flex-row gap-8 items-center">
|
||||
<div
|
||||
v-for="type in bovineType"
|
||||
:key="type.id"
|
||||
class="mt-8 flex flex-row mb-2 gap-6">
|
||||
<UiNumberInput
|
||||
:label="type.label"
|
||||
:code="type.code"
|
||||
v-model="bovineQuantities[String(type.id)]"
|
||||
:placeholder="0"
|
||||
:min="0"
|
||||
:max="10"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mt-8 flex flex-row mb-2 gap-6">
|
||||
<UiNumberInput
|
||||
label="Autres"
|
||||
v-model="otherQuantity"
|
||||
/>
|
||||
</div>
|
||||
</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 type {BovineTypeData} from "~/services/dto/bovine-type-data";
|
||||
import {getBovineTypeList} from "~/services/bovine-type";
|
||||
import {RECEPTION_TYPE_CODES} from "~/utils/constants";
|
||||
import {useReceptionStore} from '~/stores/reception'
|
||||
import {
|
||||
createReceptionBovine,
|
||||
deleteReceptionBovine,
|
||||
getReceptionBovineList,
|
||||
updateReceptionBovine
|
||||
} from "~/services/reception-bovine";
|
||||
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||
|
||||
const toast = useToast()
|
||||
const isLoadingBovineType = ref(false)
|
||||
const bovineType = ref<BovineTypeData[]>([])
|
||||
const receptionStore = useReceptionStore()
|
||||
const bovineQuantities = reactive<Record<string, number | null>>({})
|
||||
const otherQuantity = ref<number | null>(0)
|
||||
const receptionId = computed(() => receptionStore.current?.id ?? null)
|
||||
const receptionIri = computed(() =>
|
||||
receptionId.value ? `/api/receptions/${receptionId.value}` : null
|
||||
)
|
||||
const totalBovines = computed(() => {
|
||||
const base = Object.values(bovineQuantities).reduce((sum, value) => {
|
||||
return sum + (value ?? 0)
|
||||
}, 0)
|
||||
return base + (otherQuantity.value ?? 0)
|
||||
})
|
||||
|
||||
const loadBovineType = async () => {
|
||||
isLoadingBovineType.value = true
|
||||
try {
|
||||
bovineType.value = await getBovineTypeList()
|
||||
} finally {
|
||||
isLoadingBovineType.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadBovineType()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => receptionId.value,
|
||||
async (id) => {
|
||||
if (!id || !receptionIri.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectionMap: Record<string, number | null> = {}
|
||||
for (const type of bovineType.value) {
|
||||
selectionMap[String(type.id)] = 0
|
||||
}
|
||||
|
||||
const existing = await getReceptionBovineList(receptionIri.value)
|
||||
for (const selection of existing) {
|
||||
const bovineTypeId = String(selection.bovineType.id)
|
||||
selectionMap[bovineTypeId] = selection.quantity ?? 0
|
||||
}
|
||||
|
||||
for (const key of Object.keys(bovineQuantities)) {
|
||||
delete bovineQuantities[key]
|
||||
}
|
||||
Object.assign(bovineQuantities, selectionMap)
|
||||
|
||||
const existingOther = receptionStore.current?.bovineDetail
|
||||
const parsedOther =
|
||||
typeof existingOther === 'string' && existingOther.trim() !== ''
|
||||
? Number(existingOther)
|
||||
: 0
|
||||
otherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
async function syncBovineSelections(receptionIri: string) {
|
||||
const existing = await getReceptionBovineList(receptionIri)
|
||||
const existingMap = new Map<string, { id: number; quantity: number | null }>()
|
||||
|
||||
for (const selection of existing) {
|
||||
const bovineTypeId = String(selection.bovineType.id)
|
||||
existingMap.set(bovineTypeId, {
|
||||
id: selection.id,
|
||||
quantity: selection.quantity ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
// Supprime les entrées supprimées ou modifiées
|
||||
for (const [bovineTypeId, entry] of existingMap.entries()) {
|
||||
const selectedQuantity = bovineQuantities[bovineTypeId] ?? 0
|
||||
if (!selectedQuantity) {
|
||||
await deleteReceptionBovine(entry.id)
|
||||
existingMap.delete(bovineTypeId)
|
||||
continue
|
||||
}
|
||||
|
||||
if (selectedQuantity !== entry.quantity) {
|
||||
await updateReceptionBovine(entry.id, {quantity: selectedQuantity})
|
||||
existingMap.set(bovineTypeId, {
|
||||
id: entry.id,
|
||||
quantity: selectedQuantity
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Crée les entrées manquantes
|
||||
for (const [bovineTypeId, quantity] of Object.entries(bovineQuantities)) {
|
||||
if (!quantity) {
|
||||
continue
|
||||
}
|
||||
if (existingMap.has(bovineTypeId)) {
|
||||
// Déjà à jour
|
||||
continue
|
||||
}
|
||||
await createReceptionBovine({
|
||||
reception: receptionIri,
|
||||
bovineType: `/api/bovine_types/${bovineTypeId}`,
|
||||
quantity
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function goNext() {
|
||||
if (!receptionStore.current || !receptionIri.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO Ajouter un composable pour le toaster qui gère les key i18n
|
||||
if (totalBovines.value > 52) {
|
||||
toast.error({
|
||||
title: 'Erreur',
|
||||
message: ('Le total des bovins ne peut pas dépasser 52.')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
await syncBovineSelections(receptionIri.value)
|
||||
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
merchandiseType: null,
|
||||
merchandiseDetail: null,
|
||||
bovineDetail: otherQuantity.value ? String(otherQuantity.value) : null,
|
||||
currentStep: nextStep
|
||||
})
|
||||
}
|
||||
</script>
|
||||
531
frontend/components/reception/reception-form.vue
Normal file
531
frontend/components/reception/reception-form.vue
Normal file
@@ -0,0 +1,531 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Réception</h1>
|
||||
<!-- Nom de l'utilisateur -->
|
||||
<UiSelect
|
||||
id="reception-user"
|
||||
v-model="form.userId"
|
||||
label="Nom de l'utilisateur"
|
||||
:options="users.map((user) => ({
|
||||
value: String(user.id),
|
||||
label: user.username
|
||||
}))"
|
||||
:loading="isLoadingUsers"
|
||||
wrapper-class="col-start-1 row-start-2"
|
||||
/>
|
||||
<!-- Date de réception -->
|
||||
<UiDateInput
|
||||
id="reception-date"
|
||||
v-model="form.receptionDate"
|
||||
label="Date de réception"
|
||||
wrapper-class="col-start-1 row-start-3"
|
||||
/>
|
||||
<!-- Type de réception -->
|
||||
<UiSelect
|
||||
id="reception-type"
|
||||
v-model="form.receptionTypeId"
|
||||
label="Type de réception"
|
||||
:options="receptionTypes.map((type) => ({
|
||||
value: String(type.id),
|
||||
label: type.label
|
||||
}))"
|
||||
wrapper-class="col-start-1 row-start-4"
|
||||
/>
|
||||
<!-- Fournisseur -->
|
||||
<UiSelect
|
||||
id="reception-supplier"
|
||||
v-model="form.supplierId"
|
||||
label="Fournisseur"
|
||||
:options="suppliers.map((supplier) => ({
|
||||
value: String(supplier.id),
|
||||
label: supplier.name
|
||||
}))"
|
||||
:loading="isLoadingSuppliers"
|
||||
wrapper-class="col-start-1 row-start-5"
|
||||
/>
|
||||
<!-- Adresse fournisseur -->
|
||||
<UiSelect
|
||||
id="reception-address"
|
||||
v-model="form.addressId"
|
||||
label="Adresse"
|
||||
:options="supplierAddresses.map((address) => ({
|
||||
value: String(address.id),
|
||||
label: address.fullAddress
|
||||
}))"
|
||||
:disabled="isLoadingSuppliers || supplierAddresses.length === 0"
|
||||
wrapper-class="col-start-2 row-start-1"
|
||||
/>
|
||||
<!-- Camion -->
|
||||
<UiSelect
|
||||
id="reception-truck"
|
||||
v-model="form.truckId"
|
||||
label="Camion"
|
||||
:options="trucks.map((truck) => ({
|
||||
value: String(truck.id),
|
||||
label: truck.name
|
||||
}))"
|
||||
:loading="isLoadingTrucks"
|
||||
wrapper-class="col-start-2 row-start-2"
|
||||
/>
|
||||
<!-- Transporteur -->
|
||||
<UiSelect
|
||||
id="reception-carrier"
|
||||
v-model="form.carrierId"
|
||||
label="Transporteur"
|
||||
:options="carriers.map((carrier) => ({
|
||||
value: String(carrier.id),
|
||||
label: carrier.name
|
||||
}))"
|
||||
:loading="isLoadingCarriers"
|
||||
select-class="h-[34px]"
|
||||
wrapper-class="col-start-2 row-start-3"
|
||||
/>
|
||||
<!-- Chauffeur (LIOT) -->
|
||||
<UiSelect
|
||||
id="reception-driver"
|
||||
v-model="form.driverId"
|
||||
label="Nom du chauffeur si LIOT"
|
||||
:options="filteredDrivers.map((driver) => ({
|
||||
value: String(driver.id),
|
||||
label: driver.name
|
||||
}))"
|
||||
:loading="isLoadingDrivers"
|
||||
wrapper-class="col-start-2 row-start-4"
|
||||
/>
|
||||
<!-- Plaque d'immatriculation -->
|
||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
|
||||
<UiLicensePlateInput
|
||||
v-model="form.licensePlate"
|
||||
v-model:allowAny="allowAnyLicensePlate"
|
||||
/>
|
||||
</div>
|
||||
<!-- Immatriculation (LIOT) -->
|
||||
<UiSelect
|
||||
v-if="isLiotCarrier"
|
||||
id="reception-vehicle"
|
||||
v-model="form.vehicleId"
|
||||
label="Immatriculation"
|
||||
:options="filteredVehicles.map((vehicle) => ({
|
||||
value: String(vehicle.id),
|
||||
label: vehicle.plate
|
||||
}))"
|
||||
:loading="isLoadingVehicles"
|
||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||
wrapper-class="col-start-2 row-start-5"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||
>Peser
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useReceptionStore} from '~/stores/reception'
|
||||
import type {ReceptionTypeData} from '~/services/dto/reception-type-data'
|
||||
import {getReceptionTypeList} from '~/services/reception-type'
|
||||
import type {UserData} from '~/services/dto/user-data'
|
||||
import {getUsers} from '~/services/auth'
|
||||
import {useAuthStore} from '~/stores/auth'
|
||||
import type {SupplierData} from '~/services/dto/supplier-data'
|
||||
import {getSupplierList} from '~/services/supplier'
|
||||
import type {TruckData} from '~/services/dto/truck-data'
|
||||
import {getTruckList} from '~/services/truck'
|
||||
import type {CarrierData} from '~/services/dto/carrier-data'
|
||||
import {getCarrierList} from '~/services/carrier'
|
||||
import type {DriverData} from '~/services/dto/driver-data'
|
||||
import {getDriverList} from '~/services/driver'
|
||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||
import {getVehicleList} from '~/services/vehicle'
|
||||
import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
|
||||
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
||||
import type {ReceptionFormData} from "~/services/dto/reception-data";
|
||||
|
||||
const router = useRouter()
|
||||
const receptionStore = useReceptionStore()
|
||||
const form = reactive<ReceptionFormData>({
|
||||
licensePlate: '',
|
||||
receptionDate: new Date().toISOString().slice(0, 10),
|
||||
receptionTypeId: '',
|
||||
userId: '',
|
||||
supplierId: '',
|
||||
addressId: '',
|
||||
truckId: '',
|
||||
carrierId: '',
|
||||
driverId: '',
|
||||
vehicleId: ''
|
||||
})
|
||||
const allowAnyLicensePlate = ref(false)
|
||||
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||
const users = ref<UserData[]>([])
|
||||
const isLoadingUsers = ref(false)
|
||||
const suppliers = ref<SupplierData[]>([])
|
||||
const isLoadingSuppliers = ref(false)
|
||||
const trucks = ref<TruckData[]>([])
|
||||
const isLoadingTrucks = ref(false)
|
||||
const carriers = ref<CarrierData[]>([])
|
||||
const isLoadingCarriers = ref(false)
|
||||
const drivers = ref<DriverData[]>([])
|
||||
const isLoadingDrivers = ref(false)
|
||||
const vehicles = ref<VehicleData[]>([])
|
||||
const isLoadingVehicles = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
// Empêche les watchers de reset des champs pendant le remplissage initial
|
||||
const isHydrating = ref(false)
|
||||
|
||||
// Transporteur sélectionné dans le formulaire
|
||||
const selectedCarrier = computed(() =>
|
||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||
)
|
||||
// Indique si le transporteur est LIOT
|
||||
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||
// Adresses disponibles pour le fournisseur sélectionné
|
||||
const supplierAddresses = computed(() => {
|
||||
const supplierId = Number(form.supplierId)
|
||||
if (!Number.isFinite(supplierId)) {
|
||||
return []
|
||||
}
|
||||
return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? []
|
||||
})
|
||||
// Chauffeurs filtrés par transporteur (LIOT)
|
||||
const filteredDrivers = computed<DriverData[]>(() => {
|
||||
if (!form.carrierId) {
|
||||
return []
|
||||
}
|
||||
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||
})
|
||||
// Véhicules filtrés par transporteur + type de camion
|
||||
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||
if (!form.carrierId) {
|
||||
return []
|
||||
}
|
||||
return vehicles.value.filter(
|
||||
(vehicle) =>
|
||||
String(vehicle.carrier?.id) === form.carrierId &&
|
||||
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||
)
|
||||
})
|
||||
|
||||
const selectedReceptionType = computed(() =>
|
||||
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||
)
|
||||
|
||||
// Supprime les données bovines si on change de type de réception
|
||||
const clearReceptionBovines = async (receptionIri: string) => {
|
||||
const existing = await getReceptionBovineList(receptionIri)
|
||||
for (const selection of existing) {
|
||||
await deleteReceptionBovine(selection.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrate le formulaire depuis la réception en cours
|
||||
watch(
|
||||
() => receptionStore.current,
|
||||
(reception) => {
|
||||
isHydrating.value = true
|
||||
form.licensePlate = reception?.licensePlate ?? ''
|
||||
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}
|
||||
)
|
||||
|
||||
// Charge la liste des users pour le select
|
||||
const loadUsers = async () => {
|
||||
isLoadingUsers.value = true
|
||||
try {
|
||||
users.value = await getUsers()
|
||||
} finally {
|
||||
isLoadingUsers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Charge la liste des fournisseurs pour le select
|
||||
const loadSuppliers = async () => {
|
||||
isLoadingSuppliers.value = true
|
||||
try {
|
||||
suppliers.value = await getSupplierList()
|
||||
} finally {
|
||||
isLoadingSuppliers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Charge la liste des camions pour le select
|
||||
const loadTrucks = async () => {
|
||||
isLoadingTrucks.value = true
|
||||
try {
|
||||
trucks.value = await getTruckList()
|
||||
} finally {
|
||||
isLoadingTrucks.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Charge la liste des transporteurs pour le select
|
||||
const loadCarriers = async () => {
|
||||
isLoadingCarriers.value = true
|
||||
try {
|
||||
carriers.value = await getCarrierList()
|
||||
} finally {
|
||||
isLoadingCarriers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Charge la liste des chauffeurs pour le select
|
||||
const loadDrivers = async () => {
|
||||
isLoadingDrivers.value = true
|
||||
try {
|
||||
drivers.value = await getDriverList()
|
||||
} finally {
|
||||
isLoadingDrivers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Charge la liste des véhicules pour le select
|
||||
const loadVehicles = async () => {
|
||||
isLoadingVehicles.value = true
|
||||
try {
|
||||
vehicles.value = await getVehicleList()
|
||||
} finally {
|
||||
isLoadingVehicles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// On met le user connecté par défaut dans le select
|
||||
const setDefaultUser = () => {
|
||||
if (form.userId) {
|
||||
return
|
||||
}
|
||||
if (authStore.user?.id) {
|
||||
form.userId = String(authStore.user.id)
|
||||
}
|
||||
}
|
||||
|
||||
// On récupère toutes les données des selects au chargement du composant
|
||||
onMounted(async () => {
|
||||
receptionTypes.value = await getReceptionTypeList()
|
||||
await loadUsers()
|
||||
await loadSuppliers()
|
||||
await loadTrucks()
|
||||
await loadCarriers()
|
||||
await loadDrivers()
|
||||
await loadVehicles()
|
||||
await authStore.ensureSession()
|
||||
setDefaultUser()
|
||||
})
|
||||
|
||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||
watch(
|
||||
() => [form.supplierId, suppliers.value],
|
||||
() => {
|
||||
if (!form.supplierId) {
|
||||
form.addressId = ''
|
||||
return
|
||||
}
|
||||
if (!form.addressId && supplierAddresses.value.length === 1) {
|
||||
form.addressId = String(supplierAddresses.value[0].id)
|
||||
return
|
||||
}
|
||||
if (!form.addressId) {
|
||||
return
|
||||
}
|
||||
const matches = supplierAddresses.value.some(
|
||||
(address) => String(address.id) === form.addressId
|
||||
)
|
||||
if (!matches) {
|
||||
form.addressId = ''
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
||||
watch(
|
||||
() => form.carrierId,
|
||||
() => {
|
||||
if (isHydrating.value) {
|
||||
return
|
||||
}
|
||||
if (!form.carrierId) {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
return
|
||||
}
|
||||
if (!isLiotCarrier.value) {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
return
|
||||
}
|
||||
if (filteredDrivers.value.length === 1) {
|
||||
form.driverId = String(filteredDrivers.value[0].id)
|
||||
}
|
||||
if (filteredVehicles.value.length === 1) {
|
||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
||||
watch(
|
||||
() => [form.truckId, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (!isLiotCarrier.value) {
|
||||
return
|
||||
}
|
||||
if (filteredVehicles.value.length === 1) {
|
||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||
return
|
||||
}
|
||||
if (!form.vehicleId) {
|
||||
return
|
||||
}
|
||||
const matches = filteredVehicles.value.some(
|
||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||
)
|
||||
if (!matches) {
|
||||
form.vehicleId = ''
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
||||
watch(
|
||||
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (!isLiotCarrier.value) {
|
||||
return
|
||||
}
|
||||
if (isHydrating.value) {
|
||||
return
|
||||
}
|
||||
const selected = filteredVehicles.value.find(
|
||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||
)
|
||||
if (selected) {
|
||||
form.licensePlate = selected.plate
|
||||
allowAnyLicensePlate.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [form.licensePlate, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (!isLiotCarrier.value || form.vehicleId) {
|
||||
return
|
||||
}
|
||||
const match = filteredVehicles.value.find(
|
||||
(vehicle) => vehicle.plate === form.licensePlate
|
||||
)
|
||||
if (match) {
|
||||
form.vehicleId = String(match.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Valide le formulaire et crée/met à jour la réception
|
||||
async function validate() {
|
||||
const normalizedLicensePlate = form.licensePlate.trim()
|
||||
const normalizedReceptionDate = form.receptionDate.trim()
|
||||
const normalizedReceptionTypeId = form.receptionTypeId.trim()
|
||||
const normalizedUserId = form.userId.trim()
|
||||
const normalizedSupplierId = form.supplierId.trim()
|
||||
const normalizedAddressId = form.addressId.trim()
|
||||
const normalizedTruckId = form.truckId.trim()
|
||||
const normalizedCarrierId = form.carrierId.trim()
|
||||
const normalizedDriverId = form.driverId.trim()
|
||||
const receptionTypeIri = normalizedReceptionTypeId
|
||||
? `/api/reception_types/${normalizedReceptionTypeId}`
|
||||
: null
|
||||
const userIri = normalizedUserId
|
||||
? `/api/users/${normalizedUserId}`
|
||||
: null
|
||||
const supplierIri = normalizedSupplierId
|
||||
? `/api/suppliers/${normalizedSupplierId}`
|
||||
: null
|
||||
const addressIri = normalizedAddressId
|
||||
? `/api/addresses/${normalizedAddressId}`
|
||||
: null
|
||||
const truckIri = normalizedTruckId
|
||||
? `/api/trucks/${normalizedTruckId}`
|
||||
: null
|
||||
const carrierIri = normalizedCarrierId
|
||||
? `/api/carriers/${normalizedCarrierId}`
|
||||
: null
|
||||
const driverIri = normalizedDriverId
|
||||
? `/api/drivers/${normalizedDriverId}`
|
||||
: null
|
||||
|
||||
const basePayload = {
|
||||
licensePlate: normalizedLicensePlate,
|
||||
receptionDate: normalizedReceptionDate,
|
||||
receptionType: receptionTypeIri,
|
||||
user: userIri,
|
||||
supplier: supplierIri,
|
||||
address: addressIri,
|
||||
truck: truckIri,
|
||||
carrier: carrierIri
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...basePayload,
|
||||
...(isLiotCarrier.value && driverIri ? {driver: driverIri} : {})
|
||||
}
|
||||
|
||||
if (!receptionStore.current) {
|
||||
const created = await receptionStore.createReception({
|
||||
currentStep: 1,
|
||||
...payload
|
||||
})
|
||||
if (created) {
|
||||
await router.push(`/reception/${created.id}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const previousTypeCode = receptionStore.current.receptionType?.code ?? null
|
||||
const nextTypeCode = selectedReceptionType.value?.code ?? null
|
||||
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
||||
|
||||
if (
|
||||
previousTypeCode === RECEPTION_TYPE_CODES.BOVINS &&
|
||||
nextTypeCode === RECEPTION_TYPE_CODES.MERCHANDISES
|
||||
) {
|
||||
await clearReceptionBovines(receptionIri)
|
||||
}
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
currentStep: nextStep,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
255
frontend/components/reception/reception-product-received.vue
Normal file
255
frontend/components/reception/reception-product-received.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-16">
|
||||
<div
|
||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
||||
class="flex flex-col gap-16 items-center w-full">
|
||||
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1>
|
||||
<UiSelect
|
||||
id="merchandise-type"
|
||||
v-model="selectedMerchandiseTypeId"
|
||||
label="Type de marchandises"
|
||||
:options="merchandiseTypes.map((type) => ({ value: String(type.id), label: type.label }))"
|
||||
wrapper-class="w-[550px]"
|
||||
/>
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && isAutres"
|
||||
class="flex flex-col w-full max-w-[550px]"
|
||||
>
|
||||
<UiTextInput
|
||||
id="merchandise-detail"
|
||||
v-model="merchandiseDetail"
|
||||
label="Préciser"
|
||||
placeholder="Précisions complémentaires"
|
||||
:maxlength="255"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||
class="flex gap-4 w-[550px] justify-evenly"
|
||||
>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
>
|
||||
<UiCheckbox
|
||||
v-model="selectedBuildingIds"
|
||||
:value="String(building.id)"
|
||||
:label="building.label"
|
||||
label-class="text-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && isGranule"
|
||||
class="flex flex-col gap-10 w-full max-w-[1100px]"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||
<p class="font-bold uppercase">{{ type.label }}</p>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
class="flex items-center gap-2 text-lg"
|
||||
>
|
||||
<UiCheckbox
|
||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||
:value="String(building.id)"
|
||||
:label="building.label"
|
||||
label-class="text-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 {computed, onMounted, ref} from 'vue'
|
||||
import {getBuildingList} from '~/services/building'
|
||||
import {getMerchandiseTypeList} from '~/services/merchandise-type'
|
||||
import type {MerchandiseTypeData} from '~/services/dto/merchandise-type-data'
|
||||
import type {BuildingData} from '~/services/dto/building-data'
|
||||
import type {PelletTypeData} from '~/services/dto/pellet-type-data'
|
||||
import {getPelletTypeList} from '~/services/pellet-type'
|
||||
import {
|
||||
createReceptionPelletBuilding,
|
||||
deleteReceptionPelletBuilding,
|
||||
getReceptionPelletBuildingList
|
||||
} from '~/services/reception-pellet-building'
|
||||
import {useReceptionStore} from '~/stores/reception'
|
||||
import {MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES} from '~/utils/constants'
|
||||
import ReceptionBovineReceived from "~/components/reception/reception-bovine-received.vue";
|
||||
|
||||
const receptionStore = useReceptionStore()
|
||||
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
||||
const buildings = ref<BuildingData[]>([])
|
||||
const pelletTypes = ref<PelletTypeData[]>([])
|
||||
const selectedMerchandiseTypeId = ref('')
|
||||
const selectedBuildingIds = ref<string[]>([])
|
||||
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||
const merchandiseDetail = ref('')
|
||||
|
||||
// Extrait l'ID d'une relation depuis un IRI ou un objet complet.
|
||||
const getRelationId = (value: unknown): string | null => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const match = value.match(/\/(\d+)$/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && 'id' in value) {
|
||||
const record = value as { id?: number | string }
|
||||
if (typeof record.id === 'number') {
|
||||
return String(record.id)
|
||||
}
|
||||
if (typeof record.id === 'string') {
|
||||
return record.id
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Type de marchandise sélectionné dans le select
|
||||
const selectedMerchandiseType = computed(() =>
|
||||
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value)
|
||||
)
|
||||
// Indique si le type est "Granulé"
|
||||
const isGranule = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE)
|
||||
// Indique si le type est "Autres"
|
||||
const isAutres = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES)
|
||||
|
||||
// Charge les référentiels et hydrate le formulaire depuis la réception
|
||||
onMounted(async () => {
|
||||
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
||||
getMerchandiseTypeList(),
|
||||
getBuildingList(),
|
||||
getPelletTypeList()
|
||||
])
|
||||
merchandiseTypes.value = merchandiseTypeList
|
||||
buildings.value = buildingList
|
||||
pelletTypes.value = pelletTypeList
|
||||
|
||||
const currentId = receptionStore.current?.merchandiseType?.id
|
||||
if (currentId) {
|
||||
selectedMerchandiseTypeId.value = String(currentId)
|
||||
}
|
||||
merchandiseDetail.value = receptionStore.current?.merchandiseDetail ?? ''
|
||||
|
||||
selectedBuildingIds.value =
|
||||
receptionStore.current?.buildings?.map((building) => String(building.id)) ?? []
|
||||
|
||||
const existingPelletSelections = receptionStore.current?.pelletBuildings ?? []
|
||||
const selectionMap: Record<string, string[]> = {}
|
||||
for (const selection of existingPelletSelections) {
|
||||
// L'API peut renvoyer les relations comme IRI ou comme objets selon le contexte.
|
||||
const pelletTypeId = getRelationId(selection.pelletType)
|
||||
const buildingId = getRelationId(selection.building)
|
||||
if (!pelletTypeId || !buildingId) {
|
||||
continue
|
||||
}
|
||||
if (!selectionMap[pelletTypeId]) {
|
||||
selectionMap[pelletTypeId] = []
|
||||
}
|
||||
selectionMap[pelletTypeId].push(buildingId)
|
||||
}
|
||||
for (const pelletType of pelletTypes.value) {
|
||||
const key = String(pelletType.id)
|
||||
if (!selectionMap[key]) {
|
||||
selectionMap[key] = []
|
||||
}
|
||||
}
|
||||
selectedPelletBuildingIds.value = selectionMap
|
||||
})
|
||||
// Enregistre les sélections et passe à l'étape suivante
|
||||
async function goNext() {
|
||||
if (!receptionStore.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
||||
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
merchandiseType: selectedMerchandiseTypeId.value
|
||||
? `/api/merchandise_types/${selectedMerchandiseTypeId.value}`
|
||||
: null,
|
||||
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() : null,
|
||||
buildings: isGranule.value
|
||||
? []
|
||||
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
|
||||
bovineDetail: null,
|
||||
bovinesTypes: null,
|
||||
currentStep: nextStep
|
||||
})
|
||||
|
||||
if (isGranule.value) {
|
||||
await syncPelletSelections(receptionIri)
|
||||
} else {
|
||||
await clearPelletSelections(receptionIri)
|
||||
}
|
||||
}
|
||||
|
||||
// Supprime toutes les associations granulés/bâtiments existantes
|
||||
async function clearPelletSelections(receptionIri: string) {
|
||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||
for (const selection of existing) {
|
||||
await deleteReceptionPelletBuilding(selection.id)
|
||||
}
|
||||
}
|
||||
// Synchronise les associations granulés/bâtiments avec l'état du formulaire
|
||||
async function syncPelletSelections(receptionIri: string) {
|
||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||
const existingMap = new Map<string, number>()
|
||||
for (const selection of existing) {
|
||||
// Construit la table de correspondance avec des IDs normalisés pour éviter les doublons.
|
||||
const pelletTypeId = getRelationId(selection.pelletType)
|
||||
const buildingId = getRelationId(selection.building)
|
||||
if (!pelletTypeId || !buildingId) {
|
||||
continue
|
||||
}
|
||||
const key = `${pelletTypeId}:${buildingId}`
|
||||
existingMap.set(key, selection.id)
|
||||
}
|
||||
|
||||
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
|
||||
for (const [pelletTypeId, buildingIds] of Object.entries(selectedPelletBuildingIds.value)) {
|
||||
for (const buildingId of buildingIds) {
|
||||
desiredEntries.push({pelletTypeId, buildingId})
|
||||
}
|
||||
}
|
||||
|
||||
const desiredKeys = new Set(desiredEntries.map(
|
||||
(entry) => `${entry.pelletTypeId}:${entry.buildingId}`
|
||||
))
|
||||
|
||||
for (const [key, id] of existingMap.entries()) {
|
||||
if (!desiredKeys.has(key)) {
|
||||
await deleteReceptionPelletBuilding(id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of desiredEntries) {
|
||||
const key = `${entry.pelletTypeId}:${entry.buildingId}`
|
||||
if (!existingMap.has(key)) {
|
||||
await createReceptionPelletBuilding({
|
||||
reception: receptionIri,
|
||||
pelletType: `/api/pellet_types/${entry.pelletTypeId}`,
|
||||
building: `/api/buildings/${entry.buildingId}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
101
frontend/components/reception/reception-weight.vue
Normal file
101
frontend/components/reception/reception-weight.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useWeighing } from '~/composables/useWeighing'
|
||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||
import { useReceptionStore } from '~/stores/reception'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'gross' | 'tare'
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const receptionStore = useReceptionStore()
|
||||
const { current: storeReception } = storeToRefs(receptionStore)
|
||||
const { printPdf } = usePdfPrinter()
|
||||
const {
|
||||
displayWeight,
|
||||
title,
|
||||
showLoadingBox,
|
||||
fetchWeight,
|
||||
saveWeight
|
||||
} = useWeighing({
|
||||
mode: props.mode,
|
||||
reception: storeReception,
|
||||
updateReception: receptionStore.updateReception,
|
||||
loadReception: receptionStore.loadReception
|
||||
})
|
||||
// Affiche le bouton de génération du bon à l'étape tare
|
||||
const showGenerateReceipt = computed(
|
||||
() => props.mode === 'tare' && displayWeight.value !== null
|
||||
)
|
||||
|
||||
// Génère le bon de réception, puis clôture la réception
|
||||
const printReceipt = async () => {
|
||||
if (!import.meta.client || !receptionStore.current) {
|
||||
return
|
||||
}
|
||||
|
||||
await saveWeight()
|
||||
const reception = receptionStore.current
|
||||
const filename = `${reception.identificationNumber ?? reception.id}_${reception.supplier?.name ?? 'fournisseur'}_${reception.licensePlate ?? 'immat'}.pdf`
|
||||
await printPdf(`/receptions/${reception.id}/receipt`, filename)
|
||||
|
||||
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||
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('/')
|
||||
}
|
||||
|
||||
// Récupère le poids dès l'arrivée sur l'écran
|
||||
onMounted(() => {
|
||||
if (false === displayWeight.value) {
|
||||
fetchWeight()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
183
frontend/components/reception/update-bovin.vue
Normal file
183
frontend/components/reception/update-bovin.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
<div
|
||||
class="flex flex-col items-center gap-16">
|
||||
<div
|
||||
class="flex flex-row gap-6 items-center">
|
||||
<div
|
||||
v-for="type in bovineType"
|
||||
:key="type.id"
|
||||
class="flex flex-row mb-2 gap-6 ">
|
||||
<UiNumberInput
|
||||
:label="type.label"
|
||||
:code="type.code"
|
||||
v-model="bovineQuantities[String(type.id)]"
|
||||
:disabled="!auth.isAdmin"
|
||||
:placeholder="0"
|
||||
:min="0"
|
||||
:max="10"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class=" flex flex-row mb-2 gap-6">
|
||||
<UiNumberInput
|
||||
label="Autres"
|
||||
v-model="otherQuantity"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
:disabled="!auth.isAdmin"
|
||||
>Valider
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type {BovineTypeData} from "~/services/dto/bovine-type-data";
|
||||
import {getBovineTypeList} from "~/services/bovine-type";
|
||||
import {
|
||||
createReceptionBovine,
|
||||
deleteReceptionBovine,
|
||||
getReceptionBovineList,
|
||||
updateReceptionBovine
|
||||
} from "~/services/reception-bovine";
|
||||
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||
import {getReception, updateReception} from "~/services/reception";
|
||||
const toast = useToast()
|
||||
const isLoadingBovineType = ref(false)
|
||||
const bovineType = ref<BovineTypeData[]>([])
|
||||
const bovineQuantities = reactive<Record<string, number | null>>({})
|
||||
const otherQuantity = ref<number | null>(0)
|
||||
const auth = useAuthStore()
|
||||
const props = defineProps<{
|
||||
idReception: number
|
||||
}>()
|
||||
const receptionId = props.idReception
|
||||
const reception = await getReception(receptionId)
|
||||
|
||||
const receptionIri = computed(() =>
|
||||
receptionId ? `/api/receptions/${receptionId}` : 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,
|
||||
async (id) => {
|
||||
if (!id || !receptionIri.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectionMap: Record<string, number | null> = {}
|
||||
for (const type of bovineType.value) {
|
||||
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 = await reception.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 validate() {
|
||||
// @TODO Ajouter un composable pour le toaster qui gère les key i18n
|
||||
if (totalBovines.value > 52) {
|
||||
toast.error({
|
||||
title: 'Erreur',
|
||||
message: ('Le total des bovins ne peut pas dépasser 52.')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await syncBovineSelections(receptionIri.value)
|
||||
|
||||
await updateReception(receptionId, {
|
||||
merchandiseType: null,
|
||||
merchandiseDetail: null,
|
||||
bovineDetail: otherQuantity.value ? String(otherQuantity.value) : null,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
257
frontend/components/reception/update-merchandise.vue
Normal file
257
frontend/components/reception/update-merchandise.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
<div class="flex flex-col items-center gap-16">
|
||||
<div
|
||||
class="flex flex-col gap-16 items-center w-full">
|
||||
<UiTextInput
|
||||
id="merchandise-type"
|
||||
v-model="selectedMerchandiseTypeId"
|
||||
label="Type de marchandises"
|
||||
:value="reception.merchandiseType?.label"
|
||||
wrapper-class="w-[550px]"
|
||||
:disabled="true"
|
||||
/>
|
||||
<div
|
||||
v-if="merchandiseTypeId && isAutres"
|
||||
class="flex flex-col w-full max-w-[550px]"
|
||||
>
|
||||
<UiTextInput
|
||||
id="merchandise-detail"
|
||||
:disabled="!auth.isAdmin"
|
||||
v-model="merchandiseDetail"
|
||||
label="Préciser"
|
||||
placeholder="Précisions complémentaires"
|
||||
:maxlength="255"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="merchandiseTypeId && !isGranule"
|
||||
class="flex gap-4 w-[550px] justify-evenly"
|
||||
>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
>
|
||||
<UiCheckbox
|
||||
v-model="selectedBuildingIds"
|
||||
:value="String(building.id)"
|
||||
:label="building.label"
|
||||
:disabled="!auth.isAdmin"
|
||||
label-class="text-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="merchandiseTypeId && 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">{{ type.label }}</p>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
class="flex items-center gap-2 text-lg"
|
||||
>
|
||||
<UiCheckbox
|
||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||
:value="String(building.id)"
|
||||
:label="building.label"
|
||||
:disabled="!auth.isAdmin"
|
||||
label-class="text-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
:disabled="!auth.isAdmin"
|
||||
>Valider
|
||||
</button>
|
||||
</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 {MERCHANDISE_TYPE_CODES} from '~/utils/constants'
|
||||
import {getReception, updateReception} from "~/services/reception";
|
||||
|
||||
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 auth = useAuthStore()
|
||||
const props = defineProps<{
|
||||
idReception: number
|
||||
}>()
|
||||
const receptionId = props.idReception
|
||||
const reception = await getReception(receptionId)
|
||||
const merchandiseTypeId = await reception.receptionType?.id
|
||||
|
||||
// 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 = reception.merchandiseType?.id
|
||||
if (currentId) {
|
||||
selectedMerchandiseTypeId.value = String(currentId)
|
||||
}
|
||||
merchandiseDetail.value = reception.merchandiseDetail ?? ''
|
||||
|
||||
selectedBuildingIds.value =
|
||||
reception.buildings?.map((building) => String(building.id)) ?? []
|
||||
|
||||
const existingPelletSelections = reception.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 validate() {
|
||||
|
||||
const receptionIri = `/api/receptions/${reception.id}`
|
||||
|
||||
await updateReception(reception.id, {
|
||||
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() : null,
|
||||
buildings: isGranule.value
|
||||
? []
|
||||
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
|
||||
bovineDetail: null,
|
||||
bovinesTypes: null,
|
||||
})
|
||||
|
||||
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>
|
||||
74
frontend/components/reception/update-weight.vue
Normal file
74
frontend/components/reception/update-weight.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-16">
|
||||
<UiNumberInput
|
||||
label="Pesée à vide"
|
||||
v-model="form.weights[0].weight"
|
||||
:disabled="!auth.isAdmin"
|
||||
:min="0"
|
||||
/>
|
||||
|
||||
<UiNumberInput
|
||||
label="Pesée à plein"
|
||||
v-model="form.weights[1].weight"
|
||||
:disabled="!auth.isAdmin"
|
||||
:min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
:disabled="!auth.isAdmin"
|
||||
>
|
||||
Valider
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {ReceptionFormWeight} from '~/services/dto/reception-data'
|
||||
import { getReception } from '~/services/reception'
|
||||
import {updateWeight} from "~/services/weight";
|
||||
import {useAuthStore} from "~/stores/auth";
|
||||
|
||||
const props = defineProps<{
|
||||
idReception: number
|
||||
}>()
|
||||
|
||||
const idReception = props.idReception
|
||||
const auth = useAuthStore()
|
||||
|
||||
const form = reactive({
|
||||
weights: [
|
||||
{ id: 0, type: 'tare' as const, weight: 0 },
|
||||
{ id: 0, type: 'gross' as const, weight: 0 }
|
||||
]
|
||||
})
|
||||
|
||||
const hydrateFromReception = (reception: ReceptionFormWeight) => {
|
||||
const tare = reception.weights.find(weight => weight.type === 'tare')
|
||||
const gross = reception.weights.find(weight => weight.type === 'gross')
|
||||
|
||||
if (tare) form.weights[0] = { ...tare }
|
||||
if (gross) form.weights[1] = { ...gross }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const reception = await getReception(idReception)
|
||||
hydrateFromReception(reception)
|
||||
})
|
||||
|
||||
async function validate() {
|
||||
|
||||
for (const weight of form.weights) {
|
||||
if (weight.id) {
|
||||
await updateWeight(weight.id, {weight: weight.weight})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
605
frontend/components/shipment/shipment-form.vue
Normal file
605
frontend/components/shipment/shipment-form.vue
Normal file
@@ -0,0 +1,605 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Expédition</h1>
|
||||
<!-- Nom de l'utilisateur -->
|
||||
<UiSelect
|
||||
id="shipment-user"
|
||||
v-model="form.userId"
|
||||
label="Nom de l'utilisateur"
|
||||
:options="users.map((user) => ({
|
||||
value: String(user.id),
|
||||
label: user.username
|
||||
}))"
|
||||
:loading="isLoadingUsers"
|
||||
wrapper-class="col-start-1 row-start-2"
|
||||
/>
|
||||
<!-- Date de l'éxpedition -->
|
||||
<UiDateInput
|
||||
id="shipment-date"
|
||||
v-model="form.shipmentDate"
|
||||
label="Date du jour"
|
||||
wrapper-class="col-start-1 row-start-3"
|
||||
/>
|
||||
<!-- Type d'expédition -->
|
||||
<div class="col-start-1 row-start-4">
|
||||
<label class="font-bold uppercase text-xl mb-2 block">
|
||||
Type d'expédition
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-x-8">
|
||||
<div
|
||||
v-for="type in bovineShipment"
|
||||
:key="type.id"
|
||||
class="mt-8 flex flex-row gap-6"
|
||||
>
|
||||
<UiNumberInput
|
||||
:label="type.label"
|
||||
v-model="bovineQuantities[String(type.id)]"
|
||||
:placeholder="0"
|
||||
:min="0"
|
||||
:max="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Client -->
|
||||
<UiSelect
|
||||
id="shipment-customer"
|
||||
v-model="form.customerId"
|
||||
label="Client"
|
||||
:options="customers.map((customer) => ({
|
||||
value: String(customer.id),
|
||||
label: customer.label
|
||||
}))"
|
||||
:loading="isLoadingCustomers"
|
||||
wrapper-class="col-start-1 row-start-5"
|
||||
/>
|
||||
<!-- Adresse du client -->
|
||||
<UiSelect
|
||||
id="shipment-address"
|
||||
v-model="form.addressId"
|
||||
:options="customerAddressOptions"
|
||||
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||
label="Adresse"
|
||||
wrapper-class="col-start-2 row-start-1"
|
||||
/>
|
||||
<!-- Camion -->
|
||||
<UiSelect
|
||||
id="shipment-truck"
|
||||
v-model="form.truckId"
|
||||
label="Camion"
|
||||
:options="trucks.map((truck) => ({
|
||||
value: String(truck.id),
|
||||
label: truck.name
|
||||
}))"
|
||||
:loading="isLoadingTrucks"
|
||||
wrapper-class="col-start-2 row-start-2"
|
||||
/>
|
||||
<!-- Transporteur -->
|
||||
<UiSelect
|
||||
id="shipment-carrier"
|
||||
v-model="form.carrierId"
|
||||
label="Transporteur"
|
||||
:options="carriers.map((carrier) => ({
|
||||
value: String(carrier.id),
|
||||
label: carrier.name
|
||||
}))"
|
||||
wrapper-class="col-start-2 row-start-3"
|
||||
/>
|
||||
<!-- Chauffeur (LIOT) -->
|
||||
<UiSelect
|
||||
id="shipment-driver"
|
||||
v-model="form.driverId"
|
||||
label="Nom du chauffeur si LIOT"
|
||||
:options="filteredDrivers.map((driver) => ({
|
||||
value: String(driver.id),
|
||||
label: driver.name
|
||||
}))"
|
||||
:loading="isLoadingDrivers"
|
||||
wrapper-class="col-start-2 row-start-4"
|
||||
/>
|
||||
<!-- Plaque d'immatriculation (hors LIOT) -->
|
||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
|
||||
<UiLicensePlateInput
|
||||
v-model="form.licencePlate"
|
||||
v-model:allowAny="allowAnyLicensePlate"
|
||||
/>
|
||||
</div>
|
||||
<!-- Immatriculation (LIOT) -->
|
||||
<UiSelect
|
||||
v-if="isLiotCarrier"
|
||||
id="shipment-vehicle"
|
||||
v-model="form.vehicleId"
|
||||
label="Immatriculation"
|
||||
:options="filteredVehicles.map((vehicle) => ({
|
||||
value: String(vehicle.id),
|
||||
label: vehicle.plate
|
||||
}))"
|
||||
:loading="isLoadingVehicles"
|
||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||
wrapper-class="col-start-2 row-start-5"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||
>Valider
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
import type {UserData} from '~/services/dto/user-data'
|
||||
import type {CustomerData} from '~/services/dto/customer-data'
|
||||
import type {TruckData} from '~/services/dto/truck-data'
|
||||
import type {CarrierData} from '~/services/dto/carrier-data'
|
||||
import type {DriverData} from '~/services/dto/driver-data'
|
||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||
import type {AddressData} from '~/services/dto/address-data'
|
||||
import {getUsers} from '~/services/auth'
|
||||
import {getCustomerList} from '~/services/customer'
|
||||
import {getTruckList} from '~/services/truck'
|
||||
import {getCarrierList} from '~/services/carrier'
|
||||
import {getVehicleList} from '~/services/vehicle'
|
||||
import {getDriverList} from '~/services/driver'
|
||||
import type {ShipmentFormData} from '~/services/dto/shipment-data'
|
||||
import {SUPPLIER_CODE} from "~/utils/constants"
|
||||
import {useAuthStore} from '~/stores/auth'
|
||||
import {useShipmentStore} from '~/stores/shipment'
|
||||
import { computed, reactive, ref, watch, onMounted } from 'vue'
|
||||
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
|
||||
import {getShipmentTypeList} from "~/services/shipment-type";
|
||||
import {
|
||||
createShipmentBovine,
|
||||
deleteShipmentBovine,
|
||||
getBovinShipmentList,
|
||||
updateShipmentBovine
|
||||
} from "~/services/bovin-shipment";
|
||||
|
||||
const users = ref<UserData[]>([])
|
||||
const customers = ref<CustomerData[]>([])
|
||||
const trucks = ref<TruckData[]>([])
|
||||
const carriers = ref<CarrierData[]>([])
|
||||
const drivers = ref<DriverData[]>([])
|
||||
const vehicles = ref<VehicleData[]>([])
|
||||
|
||||
const isLoadingUsers = ref(false)
|
||||
const isLoadingShipmentTypes = ref(false)
|
||||
const isLoadingCustomers = ref(false)
|
||||
const isLoadingTrucks = ref(false)
|
||||
const isLoadingCarriers = ref(false)
|
||||
const isHydrating = ref(false)
|
||||
const isLoadingVehicles = ref(false)
|
||||
const allowAnyLicensePlate = ref(false)
|
||||
const isLoadingDrivers = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const shipmentStore = useShipmentStore()
|
||||
const router = useRouter()
|
||||
const bovineQuantities = ref<Record<string, number | null>>({})
|
||||
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||
// Transporteur sélectionné dans le formulaire
|
||||
const selectedCarrier = computed(() =>
|
||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||
)
|
||||
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||
|
||||
const form = reactive<ShipmentFormData>({
|
||||
userId: '',
|
||||
shipmentDate: new Date().toISOString().slice(0, 10),
|
||||
customerId: '',
|
||||
addressId: '',
|
||||
truckId: '',
|
||||
carrierId: '',
|
||||
driverId: '',
|
||||
vehicleId: '',
|
||||
licencePlate: '',
|
||||
})
|
||||
// Adresses liées au client sélectionné
|
||||
const customerAddresses = computed<AddressData[]>(() => {
|
||||
const customerId = Number(form.customerId)
|
||||
if (!Number.isFinite(customerId)) {
|
||||
return []
|
||||
}
|
||||
return customers.value.find((customer) => customer.id === customerId)?.addresses ?? []
|
||||
})
|
||||
// Options pour le select des adresses du client
|
||||
const customerAddressOptions = computed(() =>
|
||||
customerAddresses.value
|
||||
.map((address) => ({
|
||||
value: String(address.id),
|
||||
label: address.fullAddress
|
||||
}))
|
||||
)
|
||||
// Chauffeurs liés au transporteur sélectionné (LIOT)
|
||||
const filteredDrivers = computed<DriverData[]>(() => {
|
||||
if (!form.carrierId) {
|
||||
return []
|
||||
}
|
||||
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||
})
|
||||
// Véhicules liés au transporteur + camion sélectionnés (LIOT)
|
||||
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||
if (!form.carrierId) {
|
||||
return []
|
||||
}
|
||||
return vehicles.value.filter(
|
||||
(vehicle) =>
|
||||
String(vehicle.carrier?.id) === form.carrierId &&
|
||||
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||
)
|
||||
})
|
||||
// Chargement des données pour les selects
|
||||
const loadUsers = async () => {
|
||||
isLoadingUsers.value = true
|
||||
try {
|
||||
users.value = await getUsers()
|
||||
} finally {
|
||||
isLoadingUsers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadShipmentType = async () => {
|
||||
isLoadingShipmentTypes.value = true
|
||||
try {
|
||||
bovineShipment.value = await getShipmentTypeList()
|
||||
} finally {
|
||||
isLoadingShipmentTypes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadCustomers = async () => {
|
||||
isLoadingCustomers.value = true
|
||||
try {
|
||||
customers.value = await getCustomerList()
|
||||
} finally {
|
||||
isLoadingCustomers.value = false
|
||||
}
|
||||
|
||||
}
|
||||
const loadTrucks = async () => {
|
||||
isLoadingTrucks.value = true
|
||||
try {
|
||||
trucks.value = await getTruckList()
|
||||
} finally {
|
||||
isLoadingTrucks.value = false
|
||||
}
|
||||
}
|
||||
const loadCarriers = async () => {
|
||||
isLoadingCarriers.value = true
|
||||
try {
|
||||
carriers.value = await getCarrierList()
|
||||
} finally {
|
||||
isLoadingCarriers.value = false
|
||||
}
|
||||
}
|
||||
const loadVehicles = async () => {
|
||||
isLoadingVehicles.value = true
|
||||
try {
|
||||
vehicles.value = await getVehicleList()
|
||||
} finally {
|
||||
isLoadingVehicles.value = false
|
||||
}
|
||||
}
|
||||
const loadDrivers = async () => {
|
||||
isLoadingDrivers.value = true
|
||||
try {
|
||||
drivers.value = await getDriverList()
|
||||
} finally {
|
||||
isLoadingDrivers.value = false
|
||||
}
|
||||
}
|
||||
// On met le user connecté par défaut dans le select
|
||||
const setDefaultUser = () => {
|
||||
if (form.userId) {
|
||||
return
|
||||
}
|
||||
if (authStore.user?.id) {
|
||||
form.userId = String(authStore.user.id)
|
||||
}
|
||||
}
|
||||
// Chargement initial des données
|
||||
onMounted(async () => {
|
||||
await loadShipmentType()
|
||||
await loadUsers()
|
||||
await loadCustomers()
|
||||
await loadTrucks()
|
||||
await loadCarriers()
|
||||
await loadVehicles()
|
||||
await loadDrivers()
|
||||
await authStore.ensureSession()
|
||||
setDefaultUser()
|
||||
})
|
||||
// Hydrate le formulaire depuis l'expédition en cours
|
||||
watch(
|
||||
() => shipmentStore.current,
|
||||
(shipment) => {
|
||||
isHydrating.value = true
|
||||
form.licencePlate = shipment?.licencePlate ?? ''
|
||||
form.shipmentDate = shipment?.shipmentDate ?? new Date().toISOString().slice(0, 10)
|
||||
form.userId = shipment?.user?.id ? String(shipment.user.id) :
|
||||
form.userId
|
||||
form.customerId = shipment?.customer?.id ?
|
||||
String(shipment.customer.id) : ''
|
||||
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
|
||||
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
|
||||
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
|
||||
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
|
||||
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
|
||||
if (!shipment || !shipment.bovinShipments) {
|
||||
bovineQuantities.value = {}
|
||||
} else {
|
||||
const next: Record<string, number | null> = {}
|
||||
for (const entry of shipment.bovinShipments) {
|
||||
const typeId = entry.shipmentType?.id
|
||||
if (!typeId) continue
|
||||
next[String(typeId)] = entry.nbBovinSend ?? null
|
||||
}
|
||||
bovineQuantities.value = next
|
||||
}
|
||||
isHydrating.value = false
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||
watch(
|
||||
() => [form.customerId, customers.value],
|
||||
() => {
|
||||
if (!form.customerId) {
|
||||
form.addressId = ''
|
||||
return
|
||||
}
|
||||
if (!form.addressId && customerAddresses.value.length === 1) {
|
||||
form.addressId = String(customerAddresses.value[0].id)
|
||||
return
|
||||
}
|
||||
if (!form.addressId) {
|
||||
return
|
||||
}
|
||||
const matches = customerAddresses.value.some(
|
||||
(address) => String(address.id) === form.addressId
|
||||
)
|
||||
if (!matches) {
|
||||
form.addressId = ''
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
||||
const applyLiotDefaults = () => {
|
||||
if (isHydrating.value) {
|
||||
return
|
||||
}
|
||||
if (!form.carrierId) {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
return
|
||||
}
|
||||
if (!isLiotCarrier.value) {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
return
|
||||
}
|
||||
if (filteredDrivers.value.length === 1) {
|
||||
form.driverId = String(filteredDrivers.value[0].id)
|
||||
}
|
||||
if (filteredVehicles.value.length === 1) {
|
||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => form.carrierId,
|
||||
() => {
|
||||
applyLiotDefaults()
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
watch(
|
||||
() => isHydrating.value,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
applyLiotDefaults()
|
||||
}
|
||||
}
|
||||
)
|
||||
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
||||
watch(
|
||||
() => [form.truckId, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (!isLiotCarrier.value) {
|
||||
return
|
||||
}
|
||||
if (filteredVehicles.value.length === 1) {
|
||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||
return
|
||||
}
|
||||
if (!form.vehicleId) {
|
||||
return
|
||||
}
|
||||
const matches = filteredVehicles.value.some(
|
||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||
)
|
||||
if (!matches) {
|
||||
form.vehicleId = ''
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
||||
watch(
|
||||
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (!isLiotCarrier.value) {
|
||||
return
|
||||
}
|
||||
if (isHydrating.value) {
|
||||
return
|
||||
}
|
||||
const selected = filteredVehicles.value.find(
|
||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||
)
|
||||
if (selected) {
|
||||
form.licencePlate = selected.plate
|
||||
allowAnyLicensePlate.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => [form.licencePlate, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (!isLiotCarrier.value || form.vehicleId) {
|
||||
return
|
||||
}
|
||||
const match = filteredVehicles.value.find(
|
||||
(vehicle) => vehicle.plate === form.licencePlate
|
||||
)
|
||||
if (match) {
|
||||
form.vehicleId = String(match.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
const buildDesiredBovinShipments = () => {
|
||||
return bovineShipment.value
|
||||
.map((type) => {
|
||||
const raw = bovineQuantities.value[String(type.id)]
|
||||
const quantity = raw === null || raw === undefined ? 0 : Number(raw)
|
||||
return {
|
||||
type,
|
||||
quantity: Number.isFinite(quantity) ? Math.max(0, Math.trunc(quantity)) : 0
|
||||
}
|
||||
})
|
||||
.filter((entry) => entry.quantity > 0)
|
||||
}
|
||||
const syncBovinShipments = async (
|
||||
shipmentId: number,
|
||||
existing: Array<{ id?: number; nbBovinSend: number | null; shipmentType?: unknown }> = []
|
||||
) => {
|
||||
const shipmentIri = `/api/shipments/${shipmentId}`
|
||||
const desired = buildDesiredBovinShipments()
|
||||
const desiredByTypeId = new Map<number, number>()
|
||||
for (const entry of desired) {
|
||||
desiredByTypeId.set(entry.type.id, entry.quantity)
|
||||
}
|
||||
for (const entry of existing) {
|
||||
if (!entry.id) {
|
||||
continue
|
||||
}
|
||||
const rawType = entry.shipmentType
|
||||
let typeId: number | null = null
|
||||
if (rawType && typeof rawType === 'object' && 'id' in rawType) {
|
||||
typeId = Number((rawType as { id: number }).id)
|
||||
} else if (typeof rawType === 'string') {
|
||||
const match = rawType.match(/\/shipment_types\/(\\d+)$/)
|
||||
typeId = match ? Number(match[1]) : null
|
||||
}
|
||||
if (!typeId) {
|
||||
continue
|
||||
}
|
||||
const desiredQuantity = desiredByTypeId.get(typeId)
|
||||
if (!desiredQuantity) {
|
||||
await deleteShipmentBovine(entry.id)
|
||||
continue
|
||||
}
|
||||
if (entry.nbBovinSend !== desiredQuantity) {
|
||||
await updateShipmentBovine(entry.id, {nbBovinSend: desiredQuantity})
|
||||
}
|
||||
desiredByTypeId.delete(typeId)
|
||||
}
|
||||
|
||||
for (const [typeId, quantity] of desiredByTypeId.entries()) {
|
||||
await createShipmentBovine({
|
||||
shipment: shipmentIri,
|
||||
shipmentType: `/api/shipment_types/${typeId}`,
|
||||
nbBovinSend: quantity
|
||||
})
|
||||
}
|
||||
}
|
||||
const buildPayload = () => {
|
||||
const normalizedLicensePlate = form.licencePlate.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
|
||||
|
||||
return {
|
||||
licencePlate: normalizedLicensePlate,
|
||||
shipmentDate: normalizedShipmentDate,
|
||||
customer: customerIri,
|
||||
truck: truckIri,
|
||||
carrier: carrierIri,
|
||||
driver: driverIri,
|
||||
user: userIri,
|
||||
address: addressIri
|
||||
}
|
||||
}
|
||||
|
||||
const saveDraft = async () => {
|
||||
const payload = buildPayload()
|
||||
if (!shipmentStore.current) {
|
||||
const created = await shipmentStore.createShipment({
|
||||
currentStep: 0,
|
||||
...payload
|
||||
})
|
||||
if (created) {
|
||||
await syncBovinShipments(created.id, [])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||
currentStep: shipmentStore.current.currentStep,
|
||||
...payload
|
||||
})
|
||||
await syncBovinShipments(
|
||||
shipmentStore.current.id,
|
||||
shipmentStore.current?.bovinShipments ?? []
|
||||
)
|
||||
}
|
||||
|
||||
defineExpose({saveDraft})
|
||||
// Valide le formulaire et crée/met à jour l'expédition
|
||||
const validate = async () => {
|
||||
const payload = buildPayload()
|
||||
if (!shipmentStore.current) {
|
||||
const created = await shipmentStore.createShipment({
|
||||
currentStep: 1,
|
||||
...payload
|
||||
})
|
||||
if (created) {
|
||||
await shipmentStore.loadShipment(created.id)
|
||||
await syncBovinShipments(created.id, shipmentStore.current?.bovinShipments ?? [])
|
||||
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)
|
||||
await syncBovinShipments(shipmentStore.current.id, shipmentStore.current?.bovinShipments ?? [])
|
||||
}
|
||||
</script>
|
||||
101
frontend/components/shipment/shipment-weight.vue
Normal file
101
frontend/components/shipment/shipment-weight.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useWeighingShipment } from '~/composables/useWeighing'
|
||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||
import { useShipmentStore } from '~/stores/shipment'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'gross' | 'tare'
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const shipmentStore = useShipmentStore()
|
||||
const { current: storeShipment } = storeToRefs(shipmentStore)
|
||||
const { printPdf } = usePdfPrinter()
|
||||
const {
|
||||
displayWeight,
|
||||
title,
|
||||
showLoadingBox,
|
||||
fetchWeight,
|
||||
saveWeight
|
||||
} = useWeighingShipment({
|
||||
modeShipment: props.mode,
|
||||
shipment: storeShipment,
|
||||
updateShipment: shipmentStore.updateShipment,
|
||||
loadShipment: shipmentStore.loadShipment
|
||||
})
|
||||
// Affiche le bouton de génération du bon à l'étape tare
|
||||
const showGenerateReceipt = computed(
|
||||
() => props.mode === 'tare' && displayWeight.value !== null
|
||||
)
|
||||
|
||||
// Génère le bon d'expédition, puis clôture l'expédition
|
||||
const printReceipt = async () => {
|
||||
if (!import.meta.client || !shipmentStore.current) {
|
||||
return
|
||||
}
|
||||
|
||||
await saveWeight()
|
||||
const shipment = shipmentStore.current
|
||||
const filename = `${shipment.identificationNumber ?? shipment.id}_${shipment.customer?.label ?? 'client'}_${shipment.licencePlate ?? 'immat'}.pdf`
|
||||
await printPdf(`/shipments/${shipment.id}/receipt`, filename)
|
||||
|
||||
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
|
||||
const result = await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||
isValid: true
|
||||
})
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
shipmentStore.clearCurrent()
|
||||
await router.push('/')
|
||||
}
|
||||
|
||||
// Récupère le poids dès l'arrivée sur l'écran
|
||||
onMounted(() => {
|
||||
if (displayWeight.value === null) {
|
||||
fetchWeight()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
76
frontend/components/ui/UiCheckbox.vue
Normal file
76
frontend/components/ui/UiCheckbox.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label
|
||||
class="flex items-center gap-2"
|
||||
:class="labelClass"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="checked"
|
||||
:disabled="disabled"
|
||||
:class="inputClass"
|
||||
@change="onChange"
|
||||
>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
type CheckboxValue = string | number
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean | CheckboxValue[]
|
||||
value?: CheckboxValue
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
wrapperClass?: string
|
||||
labelClass?: string
|
||||
inputClass?: string
|
||||
}>(),
|
||||
{
|
||||
value: undefined,
|
||||
label: '',
|
||||
disabled: false,
|
||||
wrapperClass: '',
|
||||
labelClass: '',
|
||||
inputClass: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean | CheckboxValue[]): void
|
||||
}>()
|
||||
|
||||
const checked = computed(() => {
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
if (props.value === undefined) {
|
||||
return false
|
||||
}
|
||||
return props.modelValue.includes(props.value)
|
||||
}
|
||||
return Boolean(props.modelValue)
|
||||
})
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
if (props.value === undefined) {
|
||||
emit('update:modelValue', props.modelValue)
|
||||
return
|
||||
}
|
||||
const next = new Set(props.modelValue)
|
||||
if (target.checked) {
|
||||
next.add(props.value)
|
||||
} else {
|
||||
next.delete(props.value)
|
||||
}
|
||||
emit('update:modelValue', Array.from(next))
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', target.checked)
|
||||
}
|
||||
</script>
|
||||
62
frontend/components/ui/UiDateInput.vue
Normal file
62
frontend/components/ui/UiDateInput.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div :class="['flex flex-col', wrapperClass]">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="font-bold uppercase text-xl mb-2"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<input
|
||||
:id="id"
|
||||
type="date"
|
||||
:value="modelValue ?? ''"
|
||||
:disabled="disabled"
|
||||
v-bind="attrs"
|
||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
||||
:class="[
|
||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||
inputClass
|
||||
]"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string
|
||||
label?: string
|
||||
modelValue: string | null | undefined
|
||||
disabled?: boolean
|
||||
wrapperClass?: string
|
||||
labelClass?: string
|
||||
inputClass?: string
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
wrapperClass: '',
|
||||
labelClass: '',
|
||||
inputClass: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
const isEmpty = computed(() => !props.modelValue)
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
</script>
|
||||
92
frontend/components/ui/UiNumberInput.vue
Normal file
92
frontend/components/ui/UiNumberInput.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div :class="['flex flex-row items-center gap-2', wrapperClass]">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="text-xl text-bold flex items-center"
|
||||
: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-black text-xl bg-transparent w-12"
|
||||
: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 onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.value === '') {
|
||||
emit('update:modelValue', null)
|
||||
return
|
||||
}
|
||||
const numeric = Math.max(0, Number(target.value))
|
||||
emit('update:modelValue', Number.isNaN(numeric) ? null : numeric)
|
||||
}
|
||||
|
||||
const onKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === '-' || event.key === 'e' || event.key === 'E') {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
85
frontend/components/ui/UiSelect.vue
Normal file
85
frontend/components/ui/UiSelect.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div :class="['flex flex-col', wrapperClass]">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="font-bold uppercase text-xl mb-2"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<select
|
||||
:id="id"
|
||||
:value="modelValue ?? ''"
|
||||
:disabled="disabled || loading"
|
||||
v-bind="attrs"
|
||||
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
|
||||
:class="[
|
||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||
selectClass
|
||||
]"
|
||||
@change="onChange"
|
||||
>
|
||||
<option value="" disabled class="text-neutral-400">
|
||||
{{ placeholderText }}
|
||||
</option>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
class="text-black"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue'
|
||||
|
||||
type SelectOption = {
|
||||
value: string | number
|
||||
label: string
|
||||
}
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
modelValue: string | number | null | undefined
|
||||
options: SelectOption[]
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
wrapperClass?: string
|
||||
labelClass?: string
|
||||
selectClass?: string
|
||||
}>(),
|
||||
{
|
||||
placeholder: 'Sélectionner',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
wrapperClass: '',
|
||||
labelClass: '',
|
||||
selectClass: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const isEmpty = computed(() => props.modelValue === '' || props.modelValue === null || props.modelValue === undefined)
|
||||
const placeholderText = computed(() => props.placeholder || 'Sélectionner')
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
</script>
|
||||
68
frontend/components/ui/UiTextInput.vue
Normal file
68
frontend/components/ui/UiTextInput.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div :class="['flex flex-col', wrapperClass]">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="font-bold uppercase text-xl mb-2"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<input
|
||||
:id="id"
|
||||
type="text"
|
||||
:value="modelValue ?? ''"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
:disabled="disabled"
|
||||
v-bind="attrs"
|
||||
class="border-b border-black text-xl pb-[6px] bg-transparent"
|
||||
:class="[
|
||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||
inputClass
|
||||
]"
|
||||
@input="onInput"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string
|
||||
label?: string
|
||||
modelValue: string | null | undefined
|
||||
placeholder?: string
|
||||
maxlength?: number | string
|
||||
disabled?: boolean
|
||||
wrapperClass?: string
|
||||
labelClass?: string
|
||||
inputClass?: string
|
||||
}>(),
|
||||
{
|
||||
placeholder: '',
|
||||
maxlength: undefined,
|
||||
disabled: false,
|
||||
wrapperClass: '',
|
||||
labelClass: '',
|
||||
inputClass: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
const isEmpty = computed(() => !props.modelValue)
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
</script>
|
||||
88
frontend/components/ui/license-plate-input.vue
Normal file
88
frontend/components/ui/license-plate-input.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<label :for="inputId" class="font-bold uppercase text-xl mb-2">{{ label }}</label>
|
||||
<div class="flex items-end gap-8">
|
||||
<input
|
||||
:id="inputId"
|
||||
:value="modelValue"
|
||||
v-maska="maskOptions"
|
||||
type="text"
|
||||
:maxlength="maxLength"
|
||||
:placeholder="placeholderText"
|
||||
class="border-b border-black flex-1 min-w-0 text-xl uppercase h-[30px]"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<UiCheckbox
|
||||
:id="checkboxId"
|
||||
:model-value="allowAny"
|
||||
label="Autoriser un format libre"
|
||||
wrapper-class="ml-auto"
|
||||
label-class="gap-3 whitespace-nowrap text-sm"
|
||||
input-class="h-4 w-4 accent-primary-500"
|
||||
@update:modelValue="handleAllowAnyChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vMaska } from 'maska/vue'
|
||||
type Props = {
|
||||
modelValue: string
|
||||
allowAny?: boolean
|
||||
label?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
allowAny: false,
|
||||
label: 'Immatriculation',
|
||||
id: 'license-plate'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
(event: 'update:allowAny', value: boolean): void
|
||||
}>()
|
||||
|
||||
const inputId = computed(() => props.id)
|
||||
const checkboxId = computed(() => `${props.id}-format`)
|
||||
|
||||
const maskOptions = computed(() =>
|
||||
props.allowAny
|
||||
? undefined
|
||||
: {
|
||||
mask: '@@-###-@@',
|
||||
eager: true,
|
||||
tokens: {
|
||||
'@': {
|
||||
pattern: /[A-Za-z]/,
|
||||
transform: (char: string) => char.toUpperCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
const placeholderText = computed(() => (props.allowAny ? '' : 'AA-123-AA'))
|
||||
const maxLength = computed(() => (props.allowAny ? 20 : 9))
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.allowAny) {
|
||||
emit('update:modelValue', target.value)
|
||||
return
|
||||
}
|
||||
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const handleAllowAnyChange = (nextValue: boolean) => {
|
||||
emit('update:allowAny', nextValue)
|
||||
if (!nextValue) {
|
||||
emit('update:modelValue', props.modelValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
50
frontend/components/ui/loading-dots.vue
Normal file
50
frontend/components/ui/loading-dots.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2 text-sm uppercase">
|
||||
<span class="loader-dots">
|
||||
<span class="loader-dot"></span>
|
||||
<span class="loader-dot"></span>
|
||||
<span class="loader-dot"></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loader-dots {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loader-dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 9999px;
|
||||
background: currentColor;
|
||||
animation: loader-bounce 1s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.loader-dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.loader-dot:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes loader-bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.4;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
84
frontend/components/ui/stepper.vue
Normal file
84
frontend/components/ui/stepper.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="relative w-full">
|
||||
<div class="relative h-[18px] text-[16px] uppercase font-bold text-black mb-3">
|
||||
<div
|
||||
v-for="(label, index) in labels"
|
||||
:key="label"
|
||||
class="absolute top-0 whitespace-nowrap"
|
||||
: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>
|
||||
183
frontend/composables/useApi.ts
Normal file
183
frontend/composables/useApi.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import type { FetchOptions } from 'ofetch'
|
||||
import { $fetch, FetchError } from 'ofetch'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
export type AnyObject = Record<string, unknown>
|
||||
|
||||
export type ApiClient = {
|
||||
get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||
getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<Blob>
|
||||
post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||
put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||
patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||
delete<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||
}
|
||||
|
||||
export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
|
||||
FetchOptions<ResponseType> & {
|
||||
toast?: boolean
|
||||
toastTitle?: string
|
||||
toastErrorMessage?: string
|
||||
toastSuccessMessage?: string
|
||||
toastErrorKey?: string
|
||||
toastSuccessKey?: string
|
||||
}
|
||||
|
||||
export const useApi = (): ApiClient => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseURL = config.public.apiBase ?? '/api'
|
||||
const toast = useToast()
|
||||
const auth = useAuthStore()
|
||||
const nuxtApp = useNuxtApp()
|
||||
let isHandlingUnauthorized = false
|
||||
const i18n = nuxtApp.$i18n as
|
||||
| {
|
||||
t: (key: string) => string
|
||||
te?: (key: string) => boolean
|
||||
}
|
||||
| undefined
|
||||
const t = (key: string) => (i18n?.t ? String(i18n.t(key)) : key)
|
||||
const te = (key: string) => (i18n?.te ? i18n.te(key) : false)
|
||||
|
||||
const extractErrorMessage = (error: unknown, responseData?: unknown): string => {
|
||||
const data = responseData ?? (error as FetchError)?.data
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data
|
||||
}
|
||||
|
||||
if (data && typeof data === 'object') {
|
||||
const record = data as Record<string, unknown>
|
||||
return (
|
||||
(record['hydra:description'] as string) ||
|
||||
(record.detail as string) ||
|
||||
(record.message as string) ||
|
||||
(record.error as string) ||
|
||||
(record.title as string) ||
|
||||
(record['hydra:title'] as string) ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
return (error as FetchError)?.message ?? 'Erreur inconnue.'
|
||||
}
|
||||
|
||||
const methodErrorKeys: Record<string, string> = {
|
||||
GET: 'errors.http.get',
|
||||
POST: 'errors.http.post',
|
||||
PUT: 'errors.http.put',
|
||||
PATCH: 'errors.http.patch',
|
||||
DELETE: 'errors.http.delete'
|
||||
}
|
||||
|
||||
const client = $fetch.create({
|
||||
baseURL,
|
||||
retry: 0,
|
||||
credentials: 'include',
|
||||
onResponse({ options, response }) {
|
||||
const apiOptions = options as ApiFetchOptions<'json'>
|
||||
if (apiOptions?.toast === false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (response?.status && response.status >= 400) {
|
||||
return
|
||||
}
|
||||
|
||||
const successKey = apiOptions?.toastSuccessKey
|
||||
const successMessage =
|
||||
apiOptions?.toastSuccessMessage ||
|
||||
(successKey ? (te(successKey) ? t(successKey) : successKey) : '')
|
||||
|
||||
if (successMessage) {
|
||||
toast.success({
|
||||
title: 'Succès',
|
||||
message: successMessage
|
||||
})
|
||||
}
|
||||
},
|
||||
async onResponseError({ response, error, options }) {
|
||||
if (response?.status === 401) {
|
||||
const requestUrl = typeof options?.url === 'string' ? options.url : ''
|
||||
if (!requestUrl.includes('login_check') && !requestUrl.includes('logout')) {
|
||||
if (!isHandlingUnauthorized) {
|
||||
isHandlingUnauthorized = true
|
||||
auth.clearSession()
|
||||
const route = useRoute()
|
||||
if (route.path !== '/login') {
|
||||
await navigateTo('/login')
|
||||
}
|
||||
isHandlingUnauthorized = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const apiOptions = options as ApiFetchOptions<'json'>
|
||||
if (apiOptions?.toast === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const method =
|
||||
typeof options?.method === 'string' ? options.method.toUpperCase() : 'GET'
|
||||
const defaultKey = methodErrorKeys[method]
|
||||
const defaultMessage =
|
||||
defaultKey && te(defaultKey) ? t(defaultKey) : ''
|
||||
const errorKey = apiOptions?.toastErrorKey
|
||||
const errorMessage =
|
||||
errorKey ? (te(errorKey) ? t(errorKey) : errorKey) : ''
|
||||
const extractedMessage = extractErrorMessage(error, response?._data)
|
||||
const message =
|
||||
apiOptions?.toastErrorMessage ||
|
||||
errorMessage ||
|
||||
defaultMessage ||
|
||||
extractedMessage ||
|
||||
'Une erreur est survenue.'
|
||||
|
||||
toast.error({
|
||||
title: apiOptions?.toastTitle ?? 'Erreur',
|
||||
message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const request = <T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
||||
url: string,
|
||||
options: ApiFetchOptions<'json'> = {}
|
||||
) => {
|
||||
const needsJsonBody = method === 'POST' || method === 'PUT'
|
||||
const needsMergePatch = method === 'PATCH'
|
||||
|
||||
const headers = new Headers(options.headers as HeadersInit | undefined)
|
||||
|
||||
if (needsMergePatch && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/merge-patch+json')
|
||||
} else if (needsJsonBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
return client<T>(url, { ...options, method, headers })
|
||||
}
|
||||
|
||||
return {
|
||||
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||
return request<T>('GET', url, { ...options, query })
|
||||
},
|
||||
getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
|
||||
return client<Blob>(url, { ...options, method: 'GET', query, responseType: 'blob' })
|
||||
},
|
||||
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||
return request<T>('POST', url, { ...options, body })
|
||||
},
|
||||
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||
return request<T>('PUT', url, { ...options, body })
|
||||
},
|
||||
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||
return request<T>('PATCH', url, { ...options, body })
|
||||
},
|
||||
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||
return request<T>('DELETE', url, { ...options, query })
|
||||
}
|
||||
}
|
||||
}
|
||||
17
frontend/composables/useAppVersion.ts
Normal file
17
frontend/composables/useAppVersion.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const useAppVersion = () => {
|
||||
const api = useApi()
|
||||
const version = useState<string | null>('app-version', () => null)
|
||||
|
||||
const load = async () => {
|
||||
if (version.value) {
|
||||
return version.value
|
||||
}
|
||||
const response = await api.get<{ version: string }>('version', {}, {
|
||||
toast: false
|
||||
})
|
||||
version.value = response.version
|
||||
return version.value
|
||||
}
|
||||
|
||||
return { version, load }
|
||||
}
|
||||
29
frontend/composables/usePdfPrinter.ts
Normal file
29
frontend/composables/usePdfPrinter.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
|
||||
export const usePdfPrinter = () => {
|
||||
const api = useApi()
|
||||
|
||||
const printPdf = async (url: string, filename = 'document.pdf'): Promise<void> => {
|
||||
const blob = await api.getBlob(url)
|
||||
|
||||
const pdfBlob = blob.type === 'application/pdf'
|
||||
? blob
|
||||
: new Blob([blob], { type: 'application/pdf' })
|
||||
|
||||
const blobUrl = URL.createObjectURL(pdfBlob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = filename
|
||||
a.style.display = 'none'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
|
||||
}
|
||||
|
||||
return {
|
||||
printPdf
|
||||
}
|
||||
}
|
||||
180
frontend/composables/useWeighing.ts
Normal file
180
frontend/composables/useWeighing.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type {Ref} from 'vue'
|
||||
import {computed, ref} from 'vue'
|
||||
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
|
||||
import type {WeightData} from '~/services/dto/weight-data'
|
||||
import {getWeight} from '~/services/reception'
|
||||
import {getWeightShipment} from '~/services/shipment'
|
||||
import {createWeight, updateWeight} from '~/services/weight'
|
||||
import type {UseWeighingShipmentOptions, UseWeighingOptions} from '~/services/weight'
|
||||
import type {WeightShipmentEntryData} from "~/services/dto/shipment-data";
|
||||
|
||||
export type WeighingMode = 'gross' | 'tare'
|
||||
|
||||
|
||||
export const useWeighing = ({
|
||||
mode,
|
||||
reception,
|
||||
updateReception,
|
||||
loadReception
|
||||
}: UseWeighingOptions) => {
|
||||
const weightData = ref<WeightData | null>(null)
|
||||
const isFetching = ref(false)
|
||||
|
||||
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
||||
const weights = reception.value?.weights ?? []
|
||||
return weights.find((entry) => entry.type === mode) ?? null
|
||||
})
|
||||
|
||||
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
||||
const showLoadingBox = computed(
|
||||
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
||||
)
|
||||
|
||||
const fetchWeight = async () => {
|
||||
isFetching.value = true
|
||||
weightData.value = await getWeight().finally(() => {
|
||||
isFetching.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const saveWeight = async () => {
|
||||
if (!reception.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingEntry = currentWeightEntry.value
|
||||
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
||||
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
||||
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||
|
||||
if (baseWeight === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (existingEntry?.id) {
|
||||
await updateWeight(existingEntry.id, {
|
||||
type: mode,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
weighedAt: baseWeighedAt
|
||||
})
|
||||
} else {
|
||||
await createWeight({
|
||||
reception: `api/receptions/${reception.value.id}`,
|
||||
type: mode,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
weighedAt: baseWeighedAt
|
||||
})
|
||||
}
|
||||
|
||||
const nextStep = mode === 'tare'
|
||||
? reception.value.currentStep
|
||||
: reception.value.currentStep + 1
|
||||
await updateReception(reception.value.id, {
|
||||
currentStep: nextStep,
|
||||
isValid: reception.value.isValid
|
||||
})
|
||||
|
||||
if (loadReception) {
|
||||
await loadReception(reception.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
weightData,
|
||||
currentWeightEntry,
|
||||
displayWeight,
|
||||
displayDsd,
|
||||
title,
|
||||
showLoadingBox,
|
||||
fetchWeight,
|
||||
saveWeight
|
||||
}
|
||||
}
|
||||
|
||||
export const useWeighingShipment = ({
|
||||
modeShipment,
|
||||
shipment,
|
||||
updateShipment,
|
||||
loadShipment
|
||||
}: UseWeighingShipmentOptions) => {
|
||||
const weightData = ref<WeightData | null>(null)
|
||||
const isFetching = ref(false)
|
||||
|
||||
const currentWeightEntry = computed<WeightShipmentEntryData | null>(() => {
|
||||
const weights = shipment.value?.weights ?? []
|
||||
return weights.find((entry) => entry.type === modeShipment) ?? null
|
||||
})
|
||||
|
||||
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||
const title = computed(() => (modeShipment === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
||||
const showLoadingBox = computed(
|
||||
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
||||
)
|
||||
|
||||
const fetchWeight = async () => {
|
||||
isFetching.value = true
|
||||
weightData.value = await getWeightShipment().finally(() => {
|
||||
isFetching.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const saveWeight = async () => {
|
||||
if (!shipment.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingEntry = currentWeightEntry.value
|
||||
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
||||
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
||||
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||
|
||||
if (baseWeight === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (existingEntry?.id) {
|
||||
await updateWeight(existingEntry.id, {
|
||||
type: modeShipment,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
weighedAt: baseWeighedAt
|
||||
})
|
||||
} else {
|
||||
await createWeight({
|
||||
shipment: `api/shipments/${shipment.value.id}`,
|
||||
type: modeShipment,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
weighedAt: baseWeighedAt
|
||||
})
|
||||
}
|
||||
|
||||
const nextStep = modeShipment === 'tare'
|
||||
? shipment.value.currentStep
|
||||
: shipment.value.currentStep + 1
|
||||
await updateShipment(shipment.value.id, {
|
||||
currentStep: nextStep,
|
||||
isValid: shipment.value.isValid
|
||||
})
|
||||
|
||||
if (loadShipment) {
|
||||
await loadShipment(shipment.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
weightData,
|
||||
currentWeightEntry,
|
||||
displayWeight,
|
||||
displayDsd,
|
||||
title,
|
||||
showLoadingBox,
|
||||
fetchWeight,
|
||||
saveWeight
|
||||
}
|
||||
}
|
||||
20
frontend/constants/steps.ts
Normal file
20
frontend/constants/steps.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export enum StepLabel {
|
||||
Reception = 'Réception',
|
||||
GrossWeighing = 'Pesée à plein',
|
||||
Selection = 'Sélection réceptionnées',
|
||||
TareWeighing = 'Pesée à vide',
|
||||
Shipment = 'Expédition',
|
||||
}
|
||||
|
||||
export const RECEPTION_STEP_LABELS = [
|
||||
StepLabel.Reception,
|
||||
StepLabel.GrossWeighing,
|
||||
StepLabel.Selection,
|
||||
StepLabel.TareWeighing
|
||||
]
|
||||
|
||||
export const SHIPMENT_STEP_LABELS = [
|
||||
StepLabel.Shipment,
|
||||
StepLabel.TareWeighing,
|
||||
StepLabel.GrossWeighing,
|
||||
]
|
||||
108
frontend/i18n/locales/fr.json
Normal file
108
frontend/i18n/locales/fr.json
Normal file
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"errors": {
|
||||
"http": {
|
||||
"get": "Impossible de récupérer les données.",
|
||||
"post": "Impossible de créer la ressource.",
|
||||
"put": "Impossible de mettre à jour la ressource.",
|
||||
"patch": "Impossible de mettre à jour la ressource.",
|
||||
"delete": "Impossible de supprimer la ressource."
|
||||
},
|
||||
"reception": {
|
||||
"list": "Impossible de récupérer la liste des réceptions.",
|
||||
"fetch": "Impossible de récupérer la réception.",
|
||||
"create": "Impossible de créer la réception.",
|
||||
"update": "Impossible de mettre à jour la réception.",
|
||||
"weight": "Impossible de récupérer la pesée."
|
||||
},
|
||||
"weight": {
|
||||
"update": "Impossible de mettre à jour la pesée"
|
||||
},
|
||||
"shipment": {
|
||||
"list": "Impossible de récupérer la liste des éxpeditions.",
|
||||
"fetch": "Impossible de récupérer l'éxpeditions.",
|
||||
"create": "Impossible de créer l'éxpeditions.",
|
||||
"update": "Impossible de mettre à jour l'éxpeditions.",
|
||||
"weigh": "Impossible de récupérer la pesée."
|
||||
},
|
||||
"shipmentBovine": {
|
||||
"list": "Impossible de récupérer la liste des bovins de l'éxpedition.",
|
||||
"create": "Impossible d'enregistrer le bovin.",
|
||||
"delete": "Impossible de supprimer le bovin.",
|
||||
"update": "Impossible de mettre à jour le bovin."
|
||||
},
|
||||
"shipmentType": {
|
||||
"list": "Impossible de récupérer la liste des types d'éxpedition."
|
||||
},
|
||||
"receptionType": {
|
||||
"list": "Impossible de récupérer la liste des types de réception."
|
||||
},
|
||||
"merchandiseType": {
|
||||
"list": "Impossible de récupérer la liste des types de marchandises."
|
||||
},
|
||||
"building": {
|
||||
"list": "Impossible de récupérer la liste des bâtiments."
|
||||
},
|
||||
"pelletType": {
|
||||
"list": "Impossible de récupérer la liste des types de granulés."
|
||||
},
|
||||
"receptionPelletBuilding": {
|
||||
"list": "Impossible de récupérer la liste des dépôts de granulés.",
|
||||
"create": "Impossible d'enregistrer le dépôt de granulés.",
|
||||
"delete": "Impossible de supprimer le dépôt de granulés."
|
||||
},
|
||||
"receptionBovine": {
|
||||
"list": "Impossible de récupérer la liste des bovins de la réception.",
|
||||
"create": "Impossible d'enregistrer le bovin.",
|
||||
"delete": "Impossible de supprimer le bovin."
|
||||
},
|
||||
"supplier": {
|
||||
"list": "Impossible de récupérer la liste des fournisseurs."
|
||||
},
|
||||
"truck": {
|
||||
"list": "Impossible de récupérer la liste des camions."
|
||||
},
|
||||
"bovin": {
|
||||
"list": "Impossible de récupérer la liste des races de bovins."
|
||||
},
|
||||
"carrier": {
|
||||
"list": "Impossible de récupérer la liste des transporteurs.",
|
||||
"fetch": "Impossible de récupérer les données du transporteur",
|
||||
"update": "Impossible de mettre à jour le transporteur",
|
||||
"create": "Impossible de créer le transporteur"
|
||||
},
|
||||
"driver": {
|
||||
"list": "Impossible de récupérer la liste des chauffeurs."
|
||||
},
|
||||
"vehicle": {
|
||||
"list": "Impossible de récupérer la liste des immatriculations."
|
||||
},
|
||||
"auth": {
|
||||
"login": "Identifiants invalides.",
|
||||
"users": "Impossible de récupérer les utilisateurs.",
|
||||
"logout": "Impossible de se déconnecter.",
|
||||
"update": "Impossible de mettre à jour l'utilisateur.",
|
||||
"create": "Impossible de créer l'utilisateur."
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"reception": {
|
||||
"update": "Réception mise à jour avec succès."
|
||||
},
|
||||
"shipment": {
|
||||
"update": "Éxpedition mise à jour avec succès."
|
||||
},
|
||||
"auth": {
|
||||
"update": "Utilisateur mis à jour avec succès.",
|
||||
"create": "Utilisateur créé avec succès.",
|
||||
"login": "Connexion réussie.",
|
||||
"logout": "Déconnexion réussie."
|
||||
},
|
||||
"carrier": {
|
||||
"update": "Transporteur mis à jour",
|
||||
"create": "Transporteur créé"
|
||||
},
|
||||
"weight": {
|
||||
"update": "Pesée mis à jour"
|
||||
}
|
||||
}
|
||||
}
|
||||
74
frontend/layouts/admin.vue
Normal file
74
frontend/layouts/admin.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="min-h-screen text-neutral-900 grid grid-rows-[85px,1fr]">
|
||||
<!-- HEADER -->
|
||||
<header class="bg-primary-500 z-50 h-[85px]">
|
||||
<div class="h-full w-full px-6 grid grid-cols-[auto,1fr,auto] items-center gap-8">
|
||||
<NuxtLink to="/" class="grid place-items-center">
|
||||
<span class="grid place-items-center bg-white text-xl font-bold uppercase text-primary-500 p-4">
|
||||
LOGO
|
||||
</span>
|
||||
</NuxtLink>
|
||||
|
||||
<nav class="text-2xl font-bold uppercase text-white"></nav>
|
||||
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="text-xl font-bold uppercase text-white transition hover:opacity-80 justify-self-end"
|
||||
>
|
||||
Quitter le panel admin
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-[16rem,1fr] h-[calc(100vh-85px)] min-h-0">
|
||||
<aside class="bg-primary-500 text-white min-h-0 flex flex-col justify-between">
|
||||
<div class="flex flex-col gap-4 p-4 font-bold text-xl">
|
||||
<!-- Liste des liens à ajouter ci-dessous -->
|
||||
<NuxtLink to="/admin/dashboard">
|
||||
Tableau de bord
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin/supplier/supplier-list">
|
||||
Fournisseur
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin/carrier/carrier-list">
|
||||
Transporteur
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin/user/list">
|
||||
Utilisateurs
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<p class="font-bold text-white text-left">v{{ version }}</p>
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="min-h-0 overflow-auto px-12 py-12 ">
|
||||
<div class="w-full ">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useAuthStore} from '~/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const { version } = useAppVersion()
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await auth.logout()
|
||||
} finally {
|
||||
await navigateTo('/login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user