Compare commits
8 Commits
v0.0.5
...
0c85e48ff4
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c85e48ff4 | |||
| 29c98407f1 | |||
| 9347d00516 | |||
| 46af62483f | |||
| 6dab1d789a | |||
| cfe7baa4ae | |||
| 03638d988b | |||
| 889e8d6a09 |
55
.env
55
.env
@@ -1,22 +1,41 @@
|
|||||||
APP_ENV=
|
# In all environments, the following files are loaded if they exist,
|
||||||
APP_DEBUG=
|
# the latter taking precedence over the former:
|
||||||
APP_SECRET=
|
#
|
||||||
|
# * .env contains default values for the environment variables needed by the app
|
||||||
|
# * .env.local uncommitted file with local overrides
|
||||||
|
# * .env.$APP_ENV committed environment-specific defaults
|
||||||
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||||
|
#
|
||||||
|
# Real environment variables win over .env files.
|
||||||
|
#
|
||||||
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||||
|
# https://symfony.com/doc/current/configuration/secrets.html
|
||||||
|
#
|
||||||
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||||
|
|
||||||
DEFAULT_URI=
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
APP_SHARE_DIR=var/share
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/routing ###
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
###< symfony/routing ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||||
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
|
#
|
||||||
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||||
|
#DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
###> nelmio/cors-bundle ###
|
###> nelmio/cors-bundle ###
|
||||||
CORS_ALLOW_ORIGIN=
|
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||||
###< nelmio/cors-bundle ###
|
###< nelmio/cors-bundle ###
|
||||||
|
|
||||||
###> lexik/jwt-authentication-bundle ###
|
|
||||||
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=
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
name: Auto Tag Develop
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tag:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
persist-credentials: true
|
|
||||||
|
|
||||||
- name: Create next tag v0.0.X
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Skip if current commit already has a v0.0.* tag
|
|
||||||
if git tag --points-at HEAD | grep -qE '^v0\.0\.'; then
|
|
||||||
echo "Tag already exists on this commit. Skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
last_tag="$(git tag -l 'v0.0.*' --sort=-v:refname | head -n1 || true)"
|
|
||||||
if [ -z "$last_tag" ]; then
|
|
||||||
next_tag="v0.0.1"
|
|
||||||
else
|
|
||||||
patch="${last_tag##v0.0.}"
|
|
||||||
if ! [[ "$patch" =~ ^[0-9]+$ ]]; then
|
|
||||||
echo "Unexpected tag format: $last_tag" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
next_tag="v0.0.$((patch + 1))"
|
|
||||||
fi
|
|
||||||
|
|
||||||
git config user.name "gitea-actions"
|
|
||||||
git config user.email "gitea-actions@local"
|
|
||||||
git tag "$next_tag"
|
|
||||||
git push origin "$next_tag"
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
name: Build Release Artefact
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v0.0.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: "8.4"
|
|
||||||
extensions: mbstring, intl, pdo_pgsql, xml, curl, zip, gd
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install backend deps (prod)
|
|
||||||
env:
|
|
||||||
APP_ENV: prod
|
|
||||||
APP_DEBUG: "0"
|
|
||||||
run: composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
|
|
||||||
|
|
||||||
- name: Build frontend (static)
|
|
||||||
run: |
|
|
||||||
cd frontend
|
|
||||||
npm ci
|
|
||||||
NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ npm run generate
|
|
||||||
|
|
||||||
- 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/public
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: release/ferme-${{ github.ref_name }}.tar.gz
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
/.env.prod
|
|
||||||
/.env.local.php
|
/.env.local.php
|
||||||
/.env.*.local
|
/.env.*.local
|
||||||
/config/secrets/prod/prod.decrypt.private.php
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
/var/
|
/var/
|
||||||
/vendor/
|
/vendor/
|
||||||
/LOG/
|
/LOG/
|
||||||
/config/jwt/*.pem
|
|
||||||
###< symfony/framework-bundle ###
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
###> friendsofphp/php-cs-fixer ###
|
###> friendsofphp/php-cs-fixer ###
|
||||||
@@ -25,7 +23,3 @@
|
|||||||
###> docker ###
|
###> docker ###
|
||||||
docker/.env.docker.local
|
docker/.env.docker.local
|
||||||
###< docker ###
|
###< docker ###
|
||||||
|
|
||||||
###> lexik/jwt-authentication-bundle ###
|
|
||||||
/config/jwt/*.pem
|
|
||||||
###< lexik/jwt-authentication-bundle ###
|
|
||||||
|
|||||||
7
.idea/dataSources.xml
generated
7
.idea/dataSources.xml
generated
@@ -8,12 +8,5 @@
|
|||||||
<jdbc-url>jdbc:postgresql://localhost:5432/ferme</jdbc-url>
|
<jdbc-url>jdbc:postgresql://localhost:5432/ferme</jdbc-url>
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
</data-source>
|
</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>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
7
.idea/data_source_mapping.xml
generated
7
.idea/data_source_mapping.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DataSourcePerFileMappings">
|
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/ae622167-c834-4e7b-87a5-c1721036f5dc/console.sql" value="ae622167-c834-4e7b-87a5-c1721036f5dc" />
|
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/db-forest-config.xml
generated
6
.idea/db-forest-config.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="db-tree-configuration">
|
|
||||||
<option name="data" value="---------------------------------------- 1:0:f407a514-c6b4-4b26-9555-445a85892502 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc " />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
9
.idea/ferme.iml
generated
9
.idea/ferme.iml
generated
@@ -139,15 +139,6 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
|
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
|
<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" />
|
|
||||||
<excludePattern pattern="reference.php" />
|
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
8
.idea/php.xml
generated
8
.idea/php.xml
generated
@@ -145,14 +145,6 @@
|
|||||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||||
|
|||||||
234
.idea/workspace.xml
generated
234
.idea/workspace.xml
generated
@@ -4,9 +4,9 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="ci : fix release artefact">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : update du fichier README.md et CHANGELOG.md">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/DEPLOYMENT.md" beforeDir="false" afterPath="$PROJECT_DIR$/DEPLOYMENT.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<execution />
|
<execution />
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="151" />
|
<option name="cachedIndexableFilesCount" value="137" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="FileTemplateManagerImpl">
|
<component name="FileTemplateManagerImpl">
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="feat/202-connexion-utilisateur" />
|
<entry key="$PROJECT_DIR$" value="feat/connexion-pont-bascule" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
@@ -186,14 +186,6 @@
|
|||||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||||
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/dompdf" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-svg-lib" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/sabberworm/php-css-parser" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/dompdf/php-font-lib" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/lexik/jwt-authentication-bundle" />
|
|
||||||
<path value="$PROJECT_DIR$/vendor/lcobucci/jwt" />
|
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectColorInfo">{
|
<component name="ProjectColorInfo">{
|
||||||
@@ -213,22 +205,16 @@
|
|||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "develop",
|
"git-widget-placeholder": "feat/203-reception-parcours-pesee-multi-etapas",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "preferences.keymap",
|
"settings.editor.selected.configurable": "reference.webide.settings.project.settings.php.debug",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
},
|
},
|
||||||
"keyToStringList": {
|
"keyToStringList": {
|
||||||
"DatabaseDriversLRU": [
|
|
||||||
"postgresql"
|
|
||||||
],
|
|
||||||
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
|
||||||
"TEXT"
|
|
||||||
],
|
|
||||||
"vue.recent.templates": [
|
"vue.recent.templates": [
|
||||||
"Vue Composition API Component"
|
"Vue Composition API Component"
|
||||||
]
|
]
|
||||||
@@ -236,9 +222,6 @@
|
|||||||
}]]></component>
|
}]]></component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
<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" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages" />
|
||||||
</key>
|
</key>
|
||||||
@@ -259,11 +242,7 @@
|
|||||||
<updated>1767956826164</updated>
|
<updated>1767956826164</updated>
|
||||||
<workItem from="1767956827666" duration="7866000" />
|
<workItem from="1767956827666" duration="7866000" />
|
||||||
<workItem from="1768201706520" duration="13383000" />
|
<workItem from="1768201706520" duration="13383000" />
|
||||||
<workItem from="1768287908317" duration="28058000" />
|
<workItem from="1768287908317" duration="22144000" />
|
||||||
<workItem from="1768374298711" duration="12403000" />
|
|
||||||
<workItem from="1768460547451" duration="26946000" />
|
|
||||||
<workItem from="1768547023783" duration="11371000" />
|
|
||||||
<workItem from="1768894030675" duration="36043000" />
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
|
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -297,178 +276,18 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768316965511</updated>
|
<updated>1768316965511</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
|
<option name="localTasksCounter" value="5" />
|
||||||
<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>
|
|
||||||
<option name="localTasksCounter" value="21" />
|
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</component>
|
||||||
<component name="Vcs.Log.Tabs.Properties">
|
<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">
|
<option name="TAB_STATES">
|
||||||
<map>
|
<map>
|
||||||
<entry key="MAIN">
|
<entry key="MAIN">
|
||||||
<value>
|
<value>
|
||||||
<State>
|
<State />
|
||||||
<option name="FILTERS">
|
|
||||||
<map>
|
|
||||||
<entry key="branch">
|
|
||||||
<value>
|
|
||||||
<list>
|
|
||||||
<option value="HEAD" />
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</State>
|
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
</map>
|
</map>
|
||||||
@@ -481,25 +300,22 @@
|
|||||||
<MESSAGE value="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception" />
|
<MESSAGE value="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception" />
|
||||||
<MESSAGE value="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav" />
|
<MESSAGE value="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav" />
|
||||||
<MESSAGE value="feat : update du fichier AGENTS.md" />
|
<MESSAGE value="feat : update du fichier AGENTS.md" />
|
||||||
<MESSAGE value="feat : update du fichier README.md et CHANGELOG.md" />
|
<option name="LAST_COMMIT_MESSAGE" value="feat : update du fichier AGENTS.md" />
|
||||||
<MESSAGE value="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple" />
|
|
||||||
<MESSAGE value="test : ajout de TU sur les services et providers" />
|
|
||||||
<MESSAGE value="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global" />
|
|
||||||
<MESSAGE value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
|
|
||||||
<MESSAGE value="feat : ajout de l'authentification avec lexik" />
|
|
||||||
<MESSAGE value="feat : update du CHANGELOG.md" />
|
|
||||||
<MESSAGE value="fix : correction de l'accès au swagger en mode dev qui n'était plus accessible" />
|
|
||||||
<MESSAGE value="feat : ajout de la conf pour le déploiement en recette" />
|
|
||||||
<MESSAGE value="fix : fix de la conf pour le déploiement en recette" />
|
|
||||||
<MESSAGE value="fix : migration apache vers nginx pour un déploiement plus simple" />
|
|
||||||
<MESSAGE value="fix : dernière modification pour le déploiement en recette et le changement de conf vers nginx" />
|
|
||||||
<MESSAGE value="ci: auto tag + release artefact" />
|
|
||||||
<MESSAGE value="ci : auto tag + release artefact" />
|
|
||||||
<MESSAGE value="ci : fix release artefact" />
|
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="ci : fix release artefact" />
|
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XDebuggerManager">
|
||||||
<expand />
|
<breakpoint-manager>
|
||||||
<select />
|
<breakpoints>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/State/ReceptionWeighingProvider.php</url>
|
||||||
|
<line>27</line>
|
||||||
|
<option name="timeStamp" value="6" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/State/ReceptionWeighingProvider.php</url>
|
||||||
|
<line>22</line>
|
||||||
|
<option name="timeStamp" value="7" />
|
||||||
|
</line-breakpoint>
|
||||||
|
</breakpoints>
|
||||||
|
</breakpoint-manager>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -22,24 +22,17 @@ Frontend conventions
|
|||||||
- Layout in `frontend/layouts/default.vue`: max width `1050px`, header full width.
|
- Layout in `frontend/layouts/default.vue`: max width `1050px`, header full width.
|
||||||
- Tailwind custom color palette is `primary` (e.g. `bg-primary-500`).
|
- Tailwind custom color palette is `primary` (e.g. `bg-primary-500`).
|
||||||
- API composable in `frontend/composables/useApi.ts` with `get/post/put/patch/delete` and default JSON/PATCH content types.
|
- API 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.
|
- 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`.
|
- 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`.
|
- Weighing logic is shared via `frontend/composables/useWeighing.ts`.
|
||||||
- Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`.
|
- Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`.
|
||||||
- Active nav styles in header use `NuxtLink` with `custom` slot.
|
- 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`).
|
- 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
|
Environment & routing
|
||||||
- Frontend dev server: `npm run dev` in `frontend/`.
|
- 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`).
|
- 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.
|
- 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
|
Notes
|
||||||
- Do not add a GET that creates resources; use POST + PATCH.
|
- Do not add a GET that creates resources; use POST + PATCH.
|
||||||
|
|||||||
@@ -9,17 +9,12 @@ Ajouter dans le fichier .env
|
|||||||
- DATABASE_URL
|
- DATABASE_URL
|
||||||
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
- PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||||
- PONT_BASCULE_URL
|
- PONT_BASCULE_URL
|
||||||
- JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
|
||||||
- JWT_PUBLIC_KEY
|
|
||||||
- JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
|
||||||
- COOKIE_SECURE=0 (en dev 0 et en prod 1)
|
|
||||||
|
|
||||||
Ajouter dans le fichier .env du frontend
|
Ajouter dans le fichier .env du frontend
|
||||||
- NUXT_PUBLIC_API_BASE
|
- NUXT_PUBLIC_API_BASE
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
* [#203] Réceptions — Parcours de pesée multi-étapes (début)
|
* [#203] Réceptions — Parcours de pesée multi-étapes (début)
|
||||||
* [#202] Authentification — Connexion utilisateur (JWT)
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
# Déploiement Ferme (release Gitea)
|
|
||||||
|
|
||||||
## 1) Premier déploiement
|
|
||||||
|
|
||||||
### Pré-requis système (Ubuntu)
|
|
||||||
1. Mettre à jour la machine
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y software-properties-common ca-certificates curl gnupg unzip git nginx
|
|
||||||
```
|
|
||||||
2. Installer PHP 8.4 + FPM + extensions
|
|
||||||
```bash
|
|
||||||
sudo add-apt-repository -y ppa:ondrej/php
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y \
|
|
||||||
php8.4 php8.4-fpm php8.4-cli php8.4-common \
|
|
||||||
php8.4-mbstring php8.4-xml php8.4-curl php8.4-intl \
|
|
||||||
php8.4-zip php8.4-gd php8.4-pgsql php8.4-opcache
|
|
||||||
```
|
|
||||||
3. Installer PostgreSQL (si la DB est locale)
|
|
||||||
```bash
|
|
||||||
sudo apt install -y postgresql postgresql-contrib
|
|
||||||
sudo -u postgres psql
|
|
||||||
```
|
|
||||||
Dans psql :
|
|
||||||
```sql
|
|
||||||
CREATE USER ferme_user WITH PASSWORD 'motdepassefort';
|
|
||||||
CREATE DATABASE ferme OWNER ferme_user;
|
|
||||||
\q
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dossier de déploiement
|
|
||||||
1. Créer le dossier de déploiement
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /var/www/ferme
|
|
||||||
sudo chown -R malio:malio /var/www/ferme
|
|
||||||
```
|
|
||||||
2. Créer le fichier d’environnement
|
|
||||||
- Backend : `/var/www/ferme/.env`
|
|
||||||
- `APP_ENV=prod`
|
|
||||||
- `APP_DEBUG=0`
|
|
||||||
- `APP_SECRET=...`
|
|
||||||
- `DATABASE_URL=postgresql://ferme_user:motdepassefort@127.0.0.1:5432/ferme?serverVersion=16&charset=utf8`
|
|
||||||
- `JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem`
|
|
||||||
- `JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem`
|
|
||||||
- `JWT_PASSPHRASE=...`
|
|
||||||
- `COOKIE_SECURE=1`
|
|
||||||
- `PONT_BASCULE_BYPASS=false`
|
|
||||||
3. Générer les clés JWT
|
|
||||||
```bash
|
|
||||||
cd /var/www/ferme
|
|
||||||
mkdir -p config/jwt
|
|
||||||
php bin/console lexik:jwt:generate-keypair
|
|
||||||
```
|
|
||||||
4. Config Nginx (sous-domaine)<br>
|
|
||||||
Copier le fichier de conf /deploy/nginx/ferme.conf dans /etc/nginx/sites-available/ferme.conf
|
|
||||||
```bash
|
|
||||||
sudo ln -s /etc/nginx/sites-available/ferme.conf /etc/nginx/sites-enabled/ferme.conf
|
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
5. Installer le script de déploiement (disponible /scripts/deploy-release.sh)
|
|
||||||
```bash
|
|
||||||
sudo nano /usr/local/bin/deploy-ferme
|
|
||||||
sudo chmod +x /usr/local/bin/deploy-ferme
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2) Déployer une release
|
|
||||||
|
|
||||||
1. Créer un tag sur `develop` (auto-tag `v0.0.X`)
|
|
||||||
2. Attendre que la release Gitea soit publiée
|
|
||||||
3. Déployer la release
|
|
||||||
```bash
|
|
||||||
sudo DEPLOY_OWNER=malio /usr/local/bin/deploy-ferme v0.0.X
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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`
|
|
||||||
51
README.md
51
README.md
@@ -21,17 +21,13 @@ Dans le cas ou le `make start` plante à cause du port de la bdd, il faut modifi
|
|||||||
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
Pour les variables d'environnement, il faut demander un .env.local pour le backend et un .env pour le frontend à votre collègue.
|
||||||
|
|
||||||
Vérifier que dans le .env.local, vous avez :
|
Vérifier que dans le .env.local, vous avez :
|
||||||
* APP_SECRET (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));" et doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
* APP_SECRET (doit être différent de celui de votre collègue, puisque utilisé pour signer des tokens)
|
||||||
* DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?serverVersion=16&charset=utf8"
|
* DATABASE_URL
|
||||||
* PONT_BASCULE_BYPASS (doit être à true en dev)
|
* PONT_BASCULE_BYPASS (doit être à true en dev)
|
||||||
* PONT_BASCULE_URL
|
* PONT_BASCULE_URL
|
||||||
* JWT_SECRET_KEY (à générer avec la commande php bin/console lexik:jwt:generate-keypair)
|
|
||||||
* JWT_PUBLIC_KEY
|
|
||||||
* JWT_PASSPHRASE (à généré dans le conteneur avec la commande php -r "echo bin2hex(random_bytes(32));")
|
|
||||||
* COOKIE_SECURE=0 (en dev 0 et en prod 1)
|
|
||||||
|
|
||||||
Vérifier que dans le .env du dossier frontend, vous avez :
|
Vérifier que dans le .env du dossier frontend, vous avez :
|
||||||
* NUXT_PUBLIC_API_BASE="http://localhost:8080/api"
|
* NUXT_PUBLIC_API_BASE
|
||||||
|
|
||||||
### Configuration xdebug
|
### Configuration xdebug
|
||||||
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
Pour configurer xdebug, il faut ajouter un serveur sur phpstorm. <br>
|
||||||
@@ -58,33 +54,6 @@ make dev-nuxt
|
|||||||
```
|
```
|
||||||
Le front sera accessible sur http://localhost:3000
|
Le front sera accessible sur http://localhost:3000
|
||||||
|
|
||||||
### Authentification
|
|
||||||
Ce projet utilise l'authentification JWT avec un cookie httpOnly (LexikJWTAuthenticationBundle).
|
|
||||||
Le frontend ne lit jamais directement le token, le navigateur envoie automatiquement le cookie.
|
|
||||||
|
|
||||||
### Login flow
|
|
||||||
- Frontend envoie les identifiants à:
|
|
||||||
- `POST /api/login_check`
|
|
||||||
- Backend returns:
|
|
||||||
- `204 No Content` (normal)
|
|
||||||
- `Set-Cookie: BEARER=...; HttpOnly`
|
|
||||||
- Le cookie est automatiquement envoyé pour les futures requêtes.
|
|
||||||
- La déconnexion utilise `POST /api/logout` et redirige vers `/login`.
|
|
||||||
|
|
||||||
|
|
||||||
## 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
|
|
||||||
sudo DEPLOY_OWNER=malio /usr/local/bin/deploy-ferme vX.X.X
|
|
||||||
```
|
|
||||||
## Commandes utiles
|
## Commandes utiles
|
||||||
Pour restart le container
|
Pour restart le container
|
||||||
```bash
|
```bash
|
||||||
@@ -102,17 +71,3 @@ Pour clear le cache Symfony
|
|||||||
```bash
|
```bash
|
||||||
make cache-clear
|
make cache-clear
|
||||||
```
|
```
|
||||||
Faire une migration
|
|
||||||
```bash
|
|
||||||
make migration-migrate
|
|
||||||
```
|
|
||||||
Pour générer un password pour un user
|
|
||||||
```bash
|
|
||||||
make shell
|
|
||||||
php bin/console security:hash-password
|
|
||||||
```
|
|
||||||
Sélectionner entity User, taper sont mdp, le copier et l'ajouter dans l'insert de bdd suivant :
|
|
||||||
```sql
|
|
||||||
INSERT INTO "user" (username, roles, password)
|
|
||||||
VALUES ('Mon user', '["ROLE_USER"]', 'Mon mdp hashé');
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
"doctrine/doctrine-bundle": "^3.2",
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
"dompdf/dompdf": "^3.1",
|
|
||||||
"lexik/jwt-authentication-bundle": "*",
|
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
|
|||||||
626
composer.lock
generated
626
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "f619208e7dd3272e671e7c2b139afa87",
|
"content-hash": "3e883e3a506afa201779d16a950f4845",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/doctrine-common",
|
"name": "api-platform/doctrine-common",
|
||||||
@@ -2361,417 +2361,6 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-10-26T09:35:14+00:00"
|
"time": "2025-10-26T09:35:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "dompdf/dompdf",
|
|
||||||
"version": "v3.1.4",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/dompdf/dompdf.git",
|
|
||||||
"reference": "db712c90c5b9868df3600e64e68da62e78a34623"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/db712c90c5b9868df3600e64e68da62e78a34623",
|
|
||||||
"reference": "db712c90c5b9868df3600e64e68da62e78a34623",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"dompdf/php-font-lib": "^1.0.0",
|
|
||||||
"dompdf/php-svg-lib": "^1.0.0",
|
|
||||||
"ext-dom": "*",
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"masterminds/html5": "^2.0",
|
|
||||||
"php": "^7.1 || ^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"ext-gd": "*",
|
|
||||||
"ext-json": "*",
|
|
||||||
"ext-zip": "*",
|
|
||||||
"mockery/mockery": "^1.3",
|
|
||||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
|
|
||||||
"squizlabs/php_codesniffer": "^3.5",
|
|
||||||
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-gd": "Needed to process images",
|
|
||||||
"ext-gmagick": "Improves image processing performance",
|
|
||||||
"ext-imagick": "Improves image processing performance",
|
|
||||||
"ext-zlib": "Needed for pdf stream compression"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Dompdf\\": "src/"
|
|
||||||
},
|
|
||||||
"classmap": [
|
|
||||||
"lib/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-2.1"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "The Dompdf Community",
|
|
||||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
|
||||||
"homepage": "https://github.com/dompdf/dompdf",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/dompdf/dompdf/issues",
|
|
||||||
"source": "https://github.com/dompdf/dompdf/tree/v3.1.4"
|
|
||||||
},
|
|
||||||
"time": "2025-10-29T12:43:30+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dompdf/php-font-lib",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/dompdf/php-font-lib.git",
|
|
||||||
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
|
|
||||||
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"php": "^7.1 || ^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"FontLib\\": "src/FontLib"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-2.1-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "The FontLib Community",
|
|
||||||
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
|
||||||
"homepage": "https://github.com/dompdf/php-font-lib",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
|
||||||
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.1"
|
|
||||||
},
|
|
||||||
"time": "2024-12-02T14:37:59+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dompdf/php-svg-lib",
|
|
||||||
"version": "1.0.2",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/dompdf/php-svg-lib.git",
|
|
||||||
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
|
||||||
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"php": "^7.1 || ^8.0",
|
|
||||||
"sabberworm/php-css-parser": "^8.4 || ^9.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Svg\\": "src/Svg"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"LGPL-3.0-or-later"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "The SvgLib Community",
|
|
||||||
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A library to read, parse and export to PDF SVG files.",
|
|
||||||
"homepage": "https://github.com/dompdf/php-svg-lib",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
|
||||||
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
|
|
||||||
},
|
|
||||||
"time": "2026-01-02T16:01:13+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "lcobucci/jwt",
|
|
||||||
"version": "5.6.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/lcobucci/jwt.git",
|
|
||||||
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
|
||||||
"reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-openssl": "*",
|
|
||||||
"ext-sodium": "*",
|
|
||||||
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
|
||||||
"psr/clock": "^1.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"infection/infection": "^0.29",
|
|
||||||
"lcobucci/clock": "^3.2",
|
|
||||||
"lcobucci/coding-standard": "^11.0",
|
|
||||||
"phpbench/phpbench": "^1.2",
|
|
||||||
"phpstan/extension-installer": "^1.2",
|
|
||||||
"phpstan/phpstan": "^1.10.7",
|
|
||||||
"phpstan/phpstan-deprecation-rules": "^1.1.3",
|
|
||||||
"phpstan/phpstan-phpunit": "^1.3.10",
|
|
||||||
"phpstan/phpstan-strict-rules": "^1.5.0",
|
|
||||||
"phpunit/phpunit": "^11.1"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"lcobucci/clock": ">= 3.2"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Lcobucci\\JWT\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"BSD-3-Clause"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Luís Cobucci",
|
|
||||||
"email": "lcobucci@gmail.com",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
|
|
||||||
"keywords": [
|
|
||||||
"JWS",
|
|
||||||
"jwt"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/lcobucci/jwt/issues",
|
|
||||||
"source": "https://github.com/lcobucci/jwt/tree/5.6.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/lcobucci",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://www.patreon.com/lcobucci",
|
|
||||||
"type": "patreon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-10-17T11:30:53+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "lexik/jwt-authentication-bundle",
|
|
||||||
"version": "v3.2.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
|
|
||||||
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
|
||||||
"reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-openssl": "*",
|
|
||||||
"lcobucci/jwt": "^5.0",
|
|
||||||
"php": ">=8.2",
|
|
||||||
"symfony/clock": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/config": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/deprecation-contracts": "^2.4|^3.0",
|
|
||||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/security-bundle": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/translation-contracts": "^1.0|^2.0|^3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"api-platform/core": "^3.0|^4.0",
|
|
||||||
"rector/rector": "^1.2",
|
|
||||||
"symfony/browser-kit": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/console": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/dom-crawler": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/filesystem": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/var-dumper": "^6.4|^7.0|^8.0",
|
|
||||||
"symfony/yaml": "^6.4|^7.0|^8.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
|
|
||||||
"spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
|
|
||||||
},
|
|
||||||
"type": "symfony-bundle",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
|
|
||||||
},
|
|
||||||
"exclude-from-classmap": [
|
|
||||||
"/Tests/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Jeremy Barthe",
|
|
||||||
"email": "j.barthe@lexik.fr",
|
|
||||||
"homepage": "https://github.com/jeremyb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Nicolas Cabot",
|
|
||||||
"email": "n.cabot@lexik.fr",
|
|
||||||
"homepage": "https://github.com/slashfan"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Cedric Girard",
|
|
||||||
"email": "c.girard@lexik.fr",
|
|
||||||
"homepage": "https://github.com/cedric-g"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Dev Lexik",
|
|
||||||
"email": "dev@lexik.fr",
|
|
||||||
"homepage": "https://github.com/lexik"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Robin Chalas",
|
|
||||||
"email": "robin.chalas@gmail.com",
|
|
||||||
"homepage": "https://github.com/chalasr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Lexik Community",
|
|
||||||
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "This bundle provides JWT authentication for your Symfony REST API",
|
|
||||||
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
|
|
||||||
"keywords": [
|
|
||||||
"Authentication",
|
|
||||||
"JWS",
|
|
||||||
"api",
|
|
||||||
"bundle",
|
|
||||||
"jwt",
|
|
||||||
"rest",
|
|
||||||
"symfony"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
|
|
||||||
"source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.2.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/chalasr",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-12-20T17:47:00+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "masterminds/html5",
|
|
||||||
"version": "2.10.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/Masterminds/html5-php.git",
|
|
||||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
|
||||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-dom": "*",
|
|
||||||
"php": ">=5.3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "2.7-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Masterminds\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Matt Butcher",
|
|
||||||
"email": "technosophos@gmail.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Matt Farina",
|
|
||||||
"email": "matt@mattfarina.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Asmir Mustafic",
|
|
||||||
"email": "goetas@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "An HTML5 parser and serializer.",
|
|
||||||
"homepage": "http://masterminds.github.io/html5-php",
|
|
||||||
"keywords": [
|
|
||||||
"HTML5",
|
|
||||||
"dom",
|
|
||||||
"html",
|
|
||||||
"parser",
|
|
||||||
"querypath",
|
|
||||||
"serializer",
|
|
||||||
"xml"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
|
||||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
|
||||||
},
|
|
||||||
"time": "2025-07-25T09:04:22+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "nelmio/cors-bundle",
|
"name": "nelmio/cors-bundle",
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
@@ -3365,80 +2954,6 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-09-11T13:17:53+00:00"
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "sabberworm/php-css-parser",
|
|
||||||
"version": "v9.1.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
|
||||||
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb",
|
|
||||||
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-iconv": "*",
|
|
||||||
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
|
||||||
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"php-parallel-lint/php-parallel-lint": "1.4.0",
|
|
||||||
"phpstan/extension-installer": "1.4.3",
|
|
||||||
"phpstan/phpstan": "1.12.28 || 2.1.25",
|
|
||||||
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.7",
|
|
||||||
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6",
|
|
||||||
"phpunit/phpunit": "8.5.46",
|
|
||||||
"rawr/phpunit-data-provider": "3.3.1",
|
|
||||||
"rector/rector": "1.2.10 || 2.1.7",
|
|
||||||
"rector/type-perfect": "1.0.0 || 2.1.0"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "9.2.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Sabberworm\\CSS\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Raphael Schweikert"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Oliver Klee",
|
|
||||||
"email": "github@oliverklee.de"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Jake Hotson",
|
|
||||||
"email": "jake.github@qzdesign.co.uk"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Parser for CSS Files written in PHP",
|
|
||||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
|
||||||
"keywords": [
|
|
||||||
"css",
|
|
||||||
"parser",
|
|
||||||
"stylesheet"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
|
||||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.1.0"
|
|
||||||
},
|
|
||||||
"time": "2025-09-14T07:37:21+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/asset",
|
"name": "symfony/asset",
|
||||||
"version": "v8.0.0",
|
"version": "v8.0.0",
|
||||||
@@ -7620,145 +7135,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-04T18:17:06+00:00"
|
"time": "2025-12-04T18:17:06+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "thecodingmachine/safe",
|
|
||||||
"version": "v3.3.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/thecodingmachine/safe.git",
|
|
||||||
"reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236",
|
|
||||||
"reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^8.1"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.4",
|
|
||||||
"phpstan/phpstan": "^2",
|
|
||||||
"phpunit/phpunit": "^10",
|
|
||||||
"squizlabs/php_codesniffer": "^3.2"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"files": [
|
|
||||||
"lib/special_cases.php",
|
|
||||||
"generated/apache.php",
|
|
||||||
"generated/apcu.php",
|
|
||||||
"generated/array.php",
|
|
||||||
"generated/bzip2.php",
|
|
||||||
"generated/calendar.php",
|
|
||||||
"generated/classobj.php",
|
|
||||||
"generated/com.php",
|
|
||||||
"generated/cubrid.php",
|
|
||||||
"generated/curl.php",
|
|
||||||
"generated/datetime.php",
|
|
||||||
"generated/dir.php",
|
|
||||||
"generated/eio.php",
|
|
||||||
"generated/errorfunc.php",
|
|
||||||
"generated/exec.php",
|
|
||||||
"generated/fileinfo.php",
|
|
||||||
"generated/filesystem.php",
|
|
||||||
"generated/filter.php",
|
|
||||||
"generated/fpm.php",
|
|
||||||
"generated/ftp.php",
|
|
||||||
"generated/funchand.php",
|
|
||||||
"generated/gettext.php",
|
|
||||||
"generated/gmp.php",
|
|
||||||
"generated/gnupg.php",
|
|
||||||
"generated/hash.php",
|
|
||||||
"generated/ibase.php",
|
|
||||||
"generated/ibmDb2.php",
|
|
||||||
"generated/iconv.php",
|
|
||||||
"generated/image.php",
|
|
||||||
"generated/imap.php",
|
|
||||||
"generated/info.php",
|
|
||||||
"generated/inotify.php",
|
|
||||||
"generated/json.php",
|
|
||||||
"generated/ldap.php",
|
|
||||||
"generated/libxml.php",
|
|
||||||
"generated/lzf.php",
|
|
||||||
"generated/mailparse.php",
|
|
||||||
"generated/mbstring.php",
|
|
||||||
"generated/misc.php",
|
|
||||||
"generated/mysql.php",
|
|
||||||
"generated/mysqli.php",
|
|
||||||
"generated/network.php",
|
|
||||||
"generated/oci8.php",
|
|
||||||
"generated/opcache.php",
|
|
||||||
"generated/openssl.php",
|
|
||||||
"generated/outcontrol.php",
|
|
||||||
"generated/pcntl.php",
|
|
||||||
"generated/pcre.php",
|
|
||||||
"generated/pgsql.php",
|
|
||||||
"generated/posix.php",
|
|
||||||
"generated/ps.php",
|
|
||||||
"generated/pspell.php",
|
|
||||||
"generated/readline.php",
|
|
||||||
"generated/rnp.php",
|
|
||||||
"generated/rpminfo.php",
|
|
||||||
"generated/rrd.php",
|
|
||||||
"generated/sem.php",
|
|
||||||
"generated/session.php",
|
|
||||||
"generated/shmop.php",
|
|
||||||
"generated/sockets.php",
|
|
||||||
"generated/sodium.php",
|
|
||||||
"generated/solr.php",
|
|
||||||
"generated/spl.php",
|
|
||||||
"generated/sqlsrv.php",
|
|
||||||
"generated/ssdeep.php",
|
|
||||||
"generated/ssh2.php",
|
|
||||||
"generated/stream.php",
|
|
||||||
"generated/strings.php",
|
|
||||||
"generated/swoole.php",
|
|
||||||
"generated/uodbc.php",
|
|
||||||
"generated/uopz.php",
|
|
||||||
"generated/url.php",
|
|
||||||
"generated/var.php",
|
|
||||||
"generated/xdiff.php",
|
|
||||||
"generated/xml.php",
|
|
||||||
"generated/xmlrpc.php",
|
|
||||||
"generated/yaml.php",
|
|
||||||
"generated/yaz.php",
|
|
||||||
"generated/zip.php",
|
|
||||||
"generated/zlib.php"
|
|
||||||
],
|
|
||||||
"classmap": [
|
|
||||||
"lib/DateTime.php",
|
|
||||||
"lib/DateTimeImmutable.php",
|
|
||||||
"lib/Exceptions/",
|
|
||||||
"generated/Exceptions/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/thecodingmachine/safe/issues",
|
|
||||||
"source": "https://github.com/thecodingmachine/safe/tree/v3.3.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/OskarStark",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/shish",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/staabm",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2025-05-14T06:15:44+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "twig/twig",
|
"name": "twig/twig",
|
||||||
"version": "v3.22.2",
|
"version": "v3.22.2",
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
|
|
||||||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
|
|
||||||
use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle;
|
|
||||||
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
|
|
||||||
use Nelmio\CorsBundle\NelmioCorsBundle;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
|
||||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
|
||||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
FrameworkBundle::class => ['all' => true],
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
TwigBundle::class => ['all' => true],
|
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||||
SecurityBundle::class => ['all' => true],
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
DoctrineBundle::class => ['all' => true],
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
DoctrineMigrationsBundle::class => ['all' => true],
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
NelmioCorsBundle::class => ['all' => true],
|
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||||
LexikJWTAuthenticationBundle::class => ['all' => true],
|
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||||
ApiPlatformBundle::class => ['all' => true],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -4,7 +4,6 @@ nelmio_cors:
|
|||||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
allow_headers: ['Content-Type', 'Authorization']
|
allow_headers: ['Content-Type', 'Authorization']
|
||||||
allow_credentials: true
|
|
||||||
expose_headers: ['Link']
|
expose_headers: ['Link']
|
||||||
max_age: 3600
|
max_age: 3600
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
api_platform:
|
|
||||||
enable_docs: false
|
|
||||||
enable_swagger: false
|
|
||||||
enable_swagger_ui: false
|
|
||||||
@@ -1,43 +1,20 @@
|
|||||||
security:
|
security:
|
||||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
App\Entity\User: 'auto'
|
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
app_user_provider:
|
users_in_memory: { memory: null }
|
||||||
entity:
|
|
||||||
class: App\Entity\User
|
|
||||||
property: username
|
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
# Ensure dev tools and static assets are always allowed
|
# Ensure dev tools and static assets are always allowed
|
||||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
security: false
|
security: false
|
||||||
login:
|
main:
|
||||||
pattern: ^/login_check
|
lazy: true
|
||||||
stateless: true
|
provider: users_in_memory
|
||||||
provider: app_user_provider
|
|
||||||
json_login:
|
|
||||||
check_path: /login_check
|
|
||||||
username_path: username
|
|
||||||
password_path: password
|
|
||||||
success_handler: lexik_jwt_authentication.handler.authentication_success
|
|
||||||
failure_handler: lexik_jwt_authentication.handler.authentication_failure
|
|
||||||
api:
|
|
||||||
pattern: ^/
|
|
||||||
stateless: true
|
|
||||||
provider: app_user_provider
|
|
||||||
jwt: ~
|
|
||||||
logout:
|
|
||||||
path: /api/logout
|
|
||||||
target: /login
|
|
||||||
enable_csrf: false
|
|
||||||
delete_cookies:
|
|
||||||
BEARER:
|
|
||||||
path: /
|
|
||||||
|
|
||||||
# Activate different ways to authenticate:
|
# Activate different ways to authenticate:
|
||||||
# https://symfony.com/doc/current/security.html#the-firewall
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
@@ -47,14 +24,8 @@ security:
|
|||||||
|
|
||||||
# Note: Only the *first* matching rule is applied
|
# Note: Only the *first* matching rule is applied
|
||||||
access_control:
|
access_control:
|
||||||
# Login JWT
|
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
- { path: ^/login_check, roles: PUBLIC_ACCESS }
|
# - { path: ^/profile, roles: ROLE_USER }
|
||||||
# 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 }
|
|
||||||
# Tout le reste nécessite un JWT
|
|
||||||
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
|
|||||||
@@ -770,9 +770,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* property?: scalar|null|Param, // Default: null
|
* property?: scalar|null|Param, // Default: null
|
||||||
* manager_name?: scalar|null|Param, // Default: null
|
* manager_name?: scalar|null|Param, // Default: null
|
||||||
* },
|
* },
|
||||||
* lexik_jwt?: array{
|
|
||||||
* class?: scalar|null|Param, // Default: "Lexik\\Bundle\\JWTAuthenticationBundle\\Security\\User\\JWTUser"
|
|
||||||
* },
|
|
||||||
* }>,
|
* }>,
|
||||||
* firewalls: array<string, array{ // Default: []
|
* firewalls: array<string, array{ // Default: []
|
||||||
* pattern?: scalar|null|Param,
|
* pattern?: scalar|null|Param,
|
||||||
@@ -831,10 +828,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* provider?: scalar|null|Param,
|
* provider?: scalar|null|Param,
|
||||||
* user?: scalar|null|Param, // Default: "REMOTE_USER"
|
* user?: scalar|null|Param, // Default: "REMOTE_USER"
|
||||||
* },
|
* },
|
||||||
* jwt?: array{
|
|
||||||
* provider?: scalar|null|Param, // Default: null
|
|
||||||
* authenticator?: scalar|null|Param, // Default: "lexik_jwt_authentication.security.jwt_authenticator"
|
|
||||||
* },
|
|
||||||
* login_link?: array{
|
* login_link?: array{
|
||||||
* check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify".
|
* check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify".
|
||||||
* check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false
|
* check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false
|
||||||
@@ -1268,91 +1261,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* skip_same_as_origin?: bool|Param,
|
* skip_same_as_origin?: bool|Param,
|
||||||
* }>,
|
* }>,
|
||||||
* }
|
* }
|
||||||
* @psalm-type LexikJwtAuthenticationConfig = array{
|
|
||||||
* public_key?: scalar|null|Param, // The key used to sign tokens (useless for HMAC). If not set, the key will be automatically computed from the secret key. // Default: null
|
|
||||||
* additional_public_keys?: list<scalar|null|Param>,
|
|
||||||
* secret_key?: scalar|null|Param, // The key used to sign tokens. It can be a raw secret (for HMAC), a raw RSA/ECDSA key or the path to a file itself being plaintext or PEM. // Default: null
|
|
||||||
* pass_phrase?: scalar|null|Param, // The key passphrase (useless for HMAC) // Default: ""
|
|
||||||
* token_ttl?: scalar|null|Param, // Default: 3600
|
|
||||||
* allow_no_expiration?: bool|Param, // Allow tokens without "exp" claim (i.e. indefinitely valid, no lifetime) to be considered valid. Caution: usage of this should be rare. // Default: false
|
|
||||||
* clock_skew?: scalar|null|Param, // Default: 0
|
|
||||||
* encoder?: array{
|
|
||||||
* service?: scalar|null|Param, // Default: "lexik_jwt_authentication.encoder.lcobucci"
|
|
||||||
* signature_algorithm?: scalar|null|Param, // Default: "RS256"
|
|
||||||
* },
|
|
||||||
* user_id_claim?: scalar|null|Param, // Default: "username"
|
|
||||||
* token_extractors?: array{
|
|
||||||
* authorization_header?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: true
|
|
||||||
* prefix?: scalar|null|Param, // Default: "Bearer"
|
|
||||||
* name?: scalar|null|Param, // Default: "Authorization"
|
|
||||||
* },
|
|
||||||
* cookie?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* name?: scalar|null|Param, // Default: "BEARER"
|
|
||||||
* },
|
|
||||||
* query_parameter?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* name?: scalar|null|Param, // Default: "bearer"
|
|
||||||
* },
|
|
||||||
* split_cookie?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* cookies?: list<scalar|null|Param>,
|
|
||||||
* },
|
|
||||||
* },
|
|
||||||
* remove_token_from_body_when_cookies_used?: scalar|null|Param, // Default: true
|
|
||||||
* set_cookies?: array<string, array{ // Default: []
|
|
||||||
* lifetime?: scalar|null|Param, // The cookie lifetime. If null, the "token_ttl" option value will be used // Default: null
|
|
||||||
* samesite?: "none"|"lax"|"strict"|Param, // Default: "lax"
|
|
||||||
* path?: scalar|null|Param, // Default: "/"
|
|
||||||
* domain?: scalar|null|Param, // Default: null
|
|
||||||
* secure?: scalar|null|Param, // Default: true
|
|
||||||
* httpOnly?: scalar|null|Param, // Default: true
|
|
||||||
* partitioned?: scalar|null|Param, // Default: false
|
|
||||||
* split?: list<scalar|null|Param>,
|
|
||||||
* }>,
|
|
||||||
* api_platform?: bool|array{ // API Platform compatibility: add check_path in OpenAPI documentation.
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* check_path?: scalar|null|Param, // The login check path to add in OpenAPI. // Default: null
|
|
||||||
* username_path?: scalar|null|Param, // The path to the username in the JSON body. // Default: null
|
|
||||||
* password_path?: scalar|null|Param, // The path to the password in the JSON body. // Default: null
|
|
||||||
* },
|
|
||||||
* access_token_issuance?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* signature?: array{
|
|
||||||
* algorithm: scalar|null|Param, // The algorithm use to sign the access tokens.
|
|
||||||
* key: scalar|null|Param, // The signature key. It shall be JWK encoded.
|
|
||||||
* },
|
|
||||||
* encryption?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* key_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
|
||||||
* content_encryption_algorithm: scalar|null|Param, // The key encryption algorithm is used to encrypt the token.
|
|
||||||
* key: scalar|null|Param, // The encryption key. It shall be JWK encoded.
|
|
||||||
* },
|
|
||||||
* },
|
|
||||||
* access_token_verification?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* signature?: array{
|
|
||||||
* header_checkers?: list<scalar|null|Param>,
|
|
||||||
* claim_checkers?: list<scalar|null|Param>,
|
|
||||||
* mandatory_claims?: list<scalar|null|Param>,
|
|
||||||
* allowed_algorithms?: list<scalar|null|Param>,
|
|
||||||
* keyset: scalar|null|Param, // The signature keyset. It shall be JWKSet encoded.
|
|
||||||
* },
|
|
||||||
* encryption?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* continue_on_decryption_failure?: bool|Param, // If enable, non-encrypted tokens or tokens that failed during decryption or verification processes are accepted. // Default: false
|
|
||||||
* header_checkers?: list<scalar|null|Param>,
|
|
||||||
* allowed_key_encryption_algorithms?: list<scalar|null|Param>,
|
|
||||||
* allowed_content_encryption_algorithms?: list<scalar|null|Param>,
|
|
||||||
* keyset: scalar|null|Param, // The encryption keyset. It shall be JWKSet encoded.
|
|
||||||
* },
|
|
||||||
* },
|
|
||||||
* blocklist_token?: bool|array{
|
|
||||||
* enabled?: bool|Param, // Default: false
|
|
||||||
* cache?: scalar|null|Param, // Storage to track blocked tokens // Default: "cache.app"
|
|
||||||
* },
|
|
||||||
* }
|
|
||||||
* @psalm-type ApiPlatformConfig = array{
|
* @psalm-type ApiPlatformConfig = array{
|
||||||
* title?: scalar|null|Param, // The title of the API. // Default: ""
|
* title?: scalar|null|Param, // The title of the API. // Default: ""
|
||||||
* description?: scalar|null|Param, // The description of the API. // Default: ""
|
* description?: scalar|null|Param, // The description of the API. // Default: ""
|
||||||
@@ -1618,7 +1526,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine?: DoctrineConfig,
|
* doctrine?: DoctrineConfig,
|
||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* "when@dev"?: array{
|
* "when@dev"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
@@ -1630,7 +1537,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine?: DoctrineConfig,
|
* doctrine?: DoctrineConfig,
|
||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* },
|
* },
|
||||||
* "when@prod"?: array{
|
* "when@prod"?: array{
|
||||||
@@ -1643,7 +1549,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine?: DoctrineConfig,
|
* doctrine?: DoctrineConfig,
|
||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* },
|
* },
|
||||||
* "when@test"?: array{
|
* "when@test"?: array{
|
||||||
@@ -1656,7 +1561,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* doctrine?: DoctrineConfig,
|
* doctrine?: DoctrineConfig,
|
||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* },
|
* },
|
||||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
api_platform:
|
api_platform:
|
||||||
resource: .
|
resource: .
|
||||||
type: api_platform
|
type: api_platform
|
||||||
prefix: /api
|
prefix: /
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
_security_logout:
|
_security_logout:
|
||||||
resource: security.route_loader.logout
|
resource: security.route_loader.logout
|
||||||
type: service
|
type: service
|
||||||
|
|
||||||
api_login:
|
|
||||||
path: /login_check
|
|
||||||
methods: [POST]
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name ferme.malio-dev.fr;
|
|
||||||
|
|
||||||
root /var/www/ferme/frontend/.output/public;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location ^~ /api/ {
|
|
||||||
root /var/www/ferme/public;
|
|
||||||
try_files $uri /index.php?$query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /bundles/ {
|
|
||||||
root /var/www/ferme/public;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /api/login_check {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/ferme/public/index.php;
|
|
||||||
fastcgi_param DOCUMENT_ROOT /var/www/ferme/public;
|
|
||||||
fastcgi_param SCRIPT_NAME /index.php;
|
|
||||||
fastcgi_param PATH_INFO /login_check;
|
|
||||||
fastcgi_param REQUEST_URI /login_check;
|
|
||||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/index\.php(/|$) {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/ferme/public/index.php;
|
|
||||||
fastcgi_param DOCUMENT_ROOT /var/www/ferme/public;
|
|
||||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \.php$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
php:
|
web:
|
||||||
container_name: php-${DOCKER_APP_NAME}-fpm
|
container_name: php-${DOCKER_APP_NAME}-apache
|
||||||
build:
|
build:
|
||||||
context: ./docker/php
|
context: ./docker/php
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -20,26 +20,18 @@ services:
|
|||||||
- ~/.config:/var/www/.config # Pour la config de yarn
|
- ~/.config:/var/www/.config # Pour la config de yarn
|
||||||
- ~/.composer:/var/www/.composer # Pour la config de composer
|
- ~/.composer:/var/www/.composer # Pour la config de composer
|
||||||
- ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
|
- ./docker/php/config/php.ini:/usr/local/etc/php/php.ini
|
||||||
|
- ./docker/php/config/vhost.conf:/etc/apache2/sites-available/000-default.conf
|
||||||
- ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
- ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
|
||||||
- ./LOG:/var/www/html/LOG
|
- ./LOG:/var/www/html/LOG
|
||||||
|
- ./LOG/logs_apache:/var/log/apache2/
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
ports:
|
ports:
|
||||||
|
- "8080:80"
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
nginx:
|
|
||||||
image: nginx:1.27-alpine
|
|
||||||
container_name: nginx-${DOCKER_APP_NAME}
|
|
||||||
depends_on:
|
|
||||||
- php
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
volumes:
|
|
||||||
- ./:/var/www/html:ro
|
|
||||||
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
|
|
||||||
restart: unless-stopped
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
root /var/www/html/frontend/dist;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location ^~ /api/ {
|
|
||||||
root /var/www/html/public;
|
|
||||||
try_files $uri /index.php?$query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /bundles/ {
|
|
||||||
root /var/www/html/public;
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /api/login_check {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
|
||||||
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
|
||||||
fastcgi_param SCRIPT_NAME /index.php;
|
|
||||||
fastcgi_param PATH_INFO /login_check;
|
|
||||||
fastcgi_param REQUEST_URI /login_check;
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/index\.php(/|$) {
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
|
|
||||||
fastcgi_param DOCUMENT_ROOT /var/www/html/public;
|
|
||||||
fastcgi_pass php:9000;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \.php$ {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
ARG DOCKER_PHP_VERSION
|
ARG DOCKER_PHP_VERSION
|
||||||
|
|
||||||
FROM php:${DOCKER_PHP_VERSION}-fpm-bullseye
|
FROM php:${DOCKER_PHP_VERSION}-apache-bullseye
|
||||||
|
|
||||||
ARG DOCKER_NODE_VERSION
|
ARG DOCKER_NODE_VERSION
|
||||||
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
ENV DOCKER_NODE_VERSION="${DOCKER_NODE_VERSION}"
|
||||||
@@ -105,6 +105,10 @@ RUN rm -rf /var/cache/apk/* && rm -rf /tmp/* && \
|
|||||||
# Création de la structure du projet
|
# Création de la structure du projet
|
||||||
RUN mkdir /var/www/html/LOG
|
RUN mkdir /var/www/html/LOG
|
||||||
|
|
||||||
|
# Activation du module pour Apache2 proxy_http et rewrite
|
||||||
|
RUN a2enmod proxy_http && \
|
||||||
|
a2enmod rewrite
|
||||||
|
|
||||||
###> User ###
|
###> User ###
|
||||||
ARG CURRENT_UID
|
ARG CURRENT_UID
|
||||||
ARG CURRENT_GID
|
ARG CURRENT_GID
|
||||||
|
|||||||
32
docker/php/config/vhost.conf
Normal file
32
docker/php/config/vhost.conf
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<VirtualHost *:80>
|
||||||
|
DocumentRoot /var/www/html
|
||||||
|
|
||||||
|
AliasMatch "^/api(/.*)?" "/var/www/html/public$1"
|
||||||
|
<Directory /var/www/html/public>
|
||||||
|
Options FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule ^index\.php$ - [L]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^ /api/index.php [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
AliasMatch "^/(?!api)(.*)$" "/var/www/html/frontend/dist/$1"
|
||||||
|
<Directory /var/www/html/frontend/dist>
|
||||||
|
AllowOverride All
|
||||||
|
Order allow,deny
|
||||||
|
Allow from All
|
||||||
|
|
||||||
|
RewriteEngine on
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -d
|
||||||
|
RewriteRule ^ - [L]
|
||||||
|
RewriteRule ^ index.html [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog "${APACHE_LOG_DIR}/error.log"
|
||||||
|
CustomLog "${APACHE_LOG_DIR}/access.log" combined
|
||||||
|
</VirtualHost>
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,15 @@
|
|||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-1 items-start gap-8 mb-16">
|
<div class="grid grid-cols-1 items-start gap-8 mb-16">
|
||||||
<h1 class="font-bold text-5xl uppercase">Réception</h1>
|
<h1 class="font-bold text-5xl uppercase">Réception</h1>
|
||||||
<div>
|
<div class="flex flex-col">
|
||||||
<UiLicensePlateInput
|
<label for="license-plate" class="font-bold uppercase text-xl mb-4">Immatriculation</label>
|
||||||
|
<input
|
||||||
|
id="license-plate"
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
type="text"
|
||||||
|
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="fieldErrors.licensePlate" class="text-red-600 text-sm">{{ fieldErrors.licensePlate }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label for="reception-date" class="font-bold uppercase text-xl mb-4">Date de reception</label>
|
<label for="reception-date" class="font-bold uppercase text-xl mb-4">Date de reception</label>
|
||||||
@@ -16,19 +20,24 @@
|
|||||||
type="date"
|
type="date"
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="fieldErrors.receptionDate" class="text-red-600 text-sm">{{ fieldErrors.receptionDate }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
>Peser
|
>Valider
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="errorMessage" class="text-red-600 mt-4">{{ errorMessage }}</p>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { mapZodErrors } from '~/utils/zod-errors'
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
|
|
||||||
type ReceptionFormData = {
|
type ReceptionFormData = {
|
||||||
@@ -36,44 +45,62 @@ type ReceptionFormData = {
|
|||||||
receptionDate: string
|
receptionDate: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
|
const { errorMessage: storeErrorMessage, current: storeReception } = storeToRefs(receptionStore)
|
||||||
const form = reactive<ReceptionFormData>({
|
const form = reactive<ReceptionFormData>({
|
||||||
licensePlate: '',
|
licensePlate: '',
|
||||||
receptionDate: new Date().toISOString().slice(0, 10)
|
receptionDate: ''
|
||||||
|
})
|
||||||
|
const fieldErrors = reactive<Partial<Record<keyof ReceptionFormData, string>>>({
|
||||||
|
licensePlate: undefined,
|
||||||
|
receptionDate: undefined
|
||||||
|
})
|
||||||
|
const errorMessage = computed(() => storeErrorMessage.value)
|
||||||
|
const formSchema = z.object({
|
||||||
|
licensePlate: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Immatriculation requise.')
|
||||||
|
.max(20, 'Immatriculation trop longue (20 caracteres max).'),
|
||||||
|
receptionDate: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Date de reception requise.')
|
||||||
|
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Date de reception invalide.')
|
||||||
})
|
})
|
||||||
const allowAnyLicensePlate = ref(false)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => receptionStore.current,
|
storeReception,
|
||||||
(reception) => {
|
(reception) => {
|
||||||
form.licensePlate = reception?.licensePlate ?? ''
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
|
form.receptionDate = reception?.receptionDate ?? ''
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
async function validate() {
|
async function validate() {
|
||||||
|
if (!receptionStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldErrors.licensePlate = undefined
|
||||||
|
fieldErrors.receptionDate = undefined
|
||||||
const normalizedLicensePlate = form.licensePlate.trim()
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
const normalizedReceptionDate = form.receptionDate.trim()
|
const normalizedReceptionDate = form.receptionDate.trim()
|
||||||
|
const result = formSchema.safeParse({
|
||||||
if (!receptionStore.current) {
|
licensePlate: normalizedLicensePlate,
|
||||||
const created = await receptionStore.createReception({
|
receptionDate: normalizedReceptionDate
|
||||||
currentStep: 1,
|
})
|
||||||
licensePlate: normalizedLicensePlate,
|
if (!result.success) {
|
||||||
receptionDate: normalizedReceptionDate
|
const errors = mapZodErrors<ReceptionFormData>(result.error)
|
||||||
})
|
fieldErrors.licensePlate = errors.licensePlate ?? 'Formulaire invalide.'
|
||||||
if (created) {
|
fieldErrors.receptionDate = errors.receptionDate ?? 'Formulaire invalide.'
|
||||||
await router.push(`/reception/${created.id}`)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = receptionStore.current.currentStep + 1
|
const nextStep = receptionStore.current.currentStep + 1
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
currentStep: nextStep,
|
currentStep: nextStep,
|
||||||
licensePlate: normalizedLicensePlate,
|
licensePlate: normalizedLicensePlate || null,
|
||||||
receptionDate: normalizedReceptionDate
|
receptionDate: normalizedReceptionDate || null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<div class="flex flex-col items-center mt-[164px] gap-32">
|
<div class="flex flex-col items-center mt-[164px] gap-32">
|
||||||
<div class="flex gap-8 items-center justify-center">
|
<div class="flex gap-8 items-center justify-center">
|
||||||
<!--@TODO Prendre en compte que l'on peut aussi décharger de la marchandise-->
|
<!--@TODO Prendre en compte que l'on peut aussi décharger de la marchandise-->
|
||||||
<h1 class="text-4xl uppercase font-bold">Décharger les bêtes</h1>
|
<h1 class="text-3xl uppercase font-bold">Décharger les bêtes</h1>
|
||||||
<UiLoadingDots />
|
<UiLoadingDots />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
@click="goNext"
|
@click="goNext"
|
||||||
>Peser</button>
|
>Suivant</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,20 @@
|
|||||||
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
||||||
<div
|
<div
|
||||||
v-if="showLoadingBox"
|
v-if="errorMessage || showLoadingBox"
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
<UiLoadingDots />
|
<p v-if="errorMessage" class="text-red-500">{{ errorMessage }}</p>
|
||||||
|
<UiLoadingDots v-else />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="displayWeight !== null" class="w-full">
|
<div v-else-if="displayWeight !== null" class="w-full">
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
||||||
{{ displayWeight }} kg
|
{{ displayWeight }} kg
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="grid grid-cols-2 border border-black text-center">-->
|
<div class="grid grid-cols-2 border border-black text-center">
|
||||||
<!-- <p class="border-r border-black py-3 text-4xl font-bold">DSD</p>-->
|
<p class="border-r border-black py-3 text-4xl font-bold">DSD</p>
|
||||||
<!-- <p class="py-3 text-4xl">{{ displayDsd }}</p>-->
|
<p class="py-3 text-4xl">{{ displayDsd }}</p>
|
||||||
<!-- </div>-->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,22 +28,15 @@
|
|||||||
@click="fetchWeight"
|
@click="fetchWeight"
|
||||||
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
||||||
<button
|
<button
|
||||||
v-if="displayWeight !== null && !showGenerateReceipt"
|
v-if="displayWeight !== null"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="saveWeight"
|
@click="saveWeight"
|
||||||
>Valider la pesée</button>
|
>Valider la pesée</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>
|
</div>
|
||||||
<UiPdfPrinter ref="pdfPrinter" />
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useWeighing } from '~/composables/useWeighing'
|
import { useWeighing } from '~/composables/useWeighing'
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
@@ -51,18 +45,13 @@ const props = defineProps<{
|
|||||||
mode: 'gross' | 'tare'
|
mode: 'gross' | 'tare'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const { current: storeReception } = storeToRefs(receptionStore)
|
const { current: storeReception, errorMessage: storeErrorMessage } = storeToRefs(receptionStore)
|
||||||
type PdfPrinterHandle = {
|
|
||||||
print: (url: string) => Promise<void>
|
|
||||||
}
|
|
||||||
// Ref sur le composant d'impression pour déclencher le print() du PDF.
|
|
||||||
const pdfPrinter = ref<PdfPrinterHandle | null>(null)
|
|
||||||
const {
|
const {
|
||||||
displayWeight,
|
displayWeight,
|
||||||
displayDsd,
|
displayDsd,
|
||||||
title,
|
title,
|
||||||
|
errorMessage,
|
||||||
showLoadingBox,
|
showLoadingBox,
|
||||||
fetchWeight,
|
fetchWeight,
|
||||||
saveWeight
|
saveWeight
|
||||||
@@ -70,35 +59,9 @@ const {
|
|||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
reception: storeReception,
|
reception: storeReception,
|
||||||
updateReception: receptionStore.updateReception,
|
updateReception: receptionStore.updateReception,
|
||||||
loadReception: receptionStore.loadReception
|
loadReception: receptionStore.loadReception,
|
||||||
|
storeError: storeErrorMessage
|
||||||
})
|
})
|
||||||
const showGenerateReceipt = computed(
|
// @TODO Voir comment mettre en place la genération du bon, la validation de la reception et le dernier step
|
||||||
() => props.mode === 'tare' && displayWeight.value !== null
|
|
||||||
)
|
|
||||||
|
|
||||||
const printReceipt = async () => {
|
|
||||||
if (!import.meta.client || !receptionStore.current || !pdfPrinter.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveWeight()
|
|
||||||
await pdfPrinter.value.print(`/receptions/${receptionStore.current.id}/receipt`)
|
|
||||||
|
|
||||||
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
|
||||||
|
|
||||||
const result = await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
isValid: true
|
|
||||||
})
|
|
||||||
if (!result) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
receptionStore.clearCurrent()
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchWeight()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label :for="inputId" class="font-bold uppercase text-xl mb-4">{{ label }}</label>
|
|
||||||
<input
|
|
||||||
:id="inputId"
|
|
||||||
:value="modelValue"
|
|
||||||
v-maska="maskOptions"
|
|
||||||
type="text"
|
|
||||||
:maxlength="maxLength"
|
|
||||||
:placeholder="placeholderText"
|
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase"
|
|
||||||
@input="handleInput"
|
|
||||||
/>
|
|
||||||
<label :for="checkboxId" class="mt-3 flex items-center gap-3 text-sm">
|
|
||||||
<input
|
|
||||||
:id="checkboxId"
|
|
||||||
:checked="allowAny"
|
|
||||||
type="checkbox"
|
|
||||||
class="h-4 w-4 accent-primary-500"
|
|
||||||
@change="toggleAllowAny"
|
|
||||||
/>
|
|
||||||
Autoriser un format libre
|
|
||||||
</label>
|
|
||||||
</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 toggleAllowAny = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement | null
|
|
||||||
if (!target) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextValue = target.checked
|
|
||||||
emit('update:allowAny', nextValue)
|
|
||||||
if (!nextValue) {
|
|
||||||
emit('update:modelValue', props.modelValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<iframe ref="printFrame" class="hidden" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
|
||||||
|
|
||||||
const printFrame = ref<HTMLIFrameElement | null>(null)
|
|
||||||
const { printPdf } = usePdfPrinter()
|
|
||||||
|
|
||||||
// Expose une methode simple pour imprimer un PDF depuis les ecrans.
|
|
||||||
const print = async (url: string): Promise<void> => {
|
|
||||||
return printPdf(url, printFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
print
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,133 +1,25 @@
|
|||||||
import type { FetchOptions } from 'ofetch'
|
import type { FetchOptions } from 'ofetch'
|
||||||
import { $fetch, FetchError } from 'ofetch'
|
import { $fetch, FetchError } from 'ofetch'
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
export type AnyObject = Record<string, unknown>
|
export type AnyObject = Record<string, unknown>
|
||||||
|
|
||||||
export type ApiClient = {
|
export type ApiClient = {
|
||||||
get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
get<T>(url: string, query?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
||||||
getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<Blob>
|
post<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
||||||
post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
put<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
||||||
put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
patch<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
||||||
patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
delete<T>(url: string, query?: AnyObject, options?: FetchOptions<'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 => {
|
export const useApi = (): ApiClient => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const baseURL = config.public.apiBase ?? '/api'
|
const baseURL = config.public.apiBase ?? '/api'
|
||||||
const toast = useToast()
|
const client = $fetch.create({ baseURL })
|
||||||
const auth = useAuthStore()
|
|
||||||
const nuxtApp = useNuxtApp()
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onResponseError({ response, error, options }) {
|
|
||||||
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>(
|
const request = <T>(
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
||||||
url: string,
|
url: string,
|
||||||
options: ApiFetchOptions<'json'> = {}
|
options: FetchOptions<'json'> = {}
|
||||||
) => {
|
) => {
|
||||||
const needsJsonBody = method === 'POST' || method === 'PUT'
|
const needsJsonBody = method === 'POST' || method === 'PUT'
|
||||||
const needsMergePatch = method === 'PATCH'
|
const needsMergePatch = method === 'PATCH'
|
||||||
@@ -144,22 +36,19 @@ export const useApi = (): ApiClient => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
get<T>(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
||||||
return request<T>('GET', url, { ...options, query })
|
return request<T>('GET', url, { ...options, query })
|
||||||
},
|
},
|
||||||
getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
|
post<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
||||||
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 })
|
return request<T>('POST', url, { ...options, body })
|
||||||
},
|
},
|
||||||
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
put<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
||||||
return request<T>('PUT', url, { ...options, body })
|
return request<T>('PUT', url, { ...options, body })
|
||||||
},
|
},
|
||||||
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
patch<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
||||||
return request<T>('PATCH', url, { ...options, body })
|
return request<T>('PATCH', url, { ...options, body })
|
||||||
},
|
},
|
||||||
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
delete<T>(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
||||||
return request<T>('DELETE', url, { ...options, query })
|
return request<T>('DELETE', url, { ...options, query })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import type { Ref } from 'vue'
|
|
||||||
import { useApi } from '~/composables/useApi'
|
|
||||||
|
|
||||||
type PrintFrameRef = Ref<HTMLIFrameElement | null>
|
|
||||||
|
|
||||||
export const usePdfPrinter = () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const printPdf = async (url: string, frameRef: PrintFrameRef): Promise<void> => {
|
|
||||||
if (!import.meta.client) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = frameRef.value
|
|
||||||
if (!frame) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// On charge le PDF en blob pour rester en same-origin dans l'iframe.
|
|
||||||
const blob = await api.getBlob(url)
|
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
const tryPrint = () => {
|
|
||||||
frame.contentWindow?.focus()
|
|
||||||
frame.contentWindow?.print()
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.onload = () => {
|
|
||||||
tryPrint()
|
|
||||||
// On libere l'URL blob apres l'impression.
|
|
||||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 2000)
|
|
||||||
}
|
|
||||||
frame.src = blobUrl
|
|
||||||
setTimeout(tryPrint, 1200)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
printPdf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import type {Ref} from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import {computed, ref} from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type {ReceptionData, WeightEntryData} from '~/services/dto/reception-data'
|
import type { ReceptionData, WeightEntryData } from '~/services/dto/reception-data'
|
||||||
import type {WeightData} from '~/services/dto/weight-data'
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
import {getWeight} from '~/services/reception'
|
import { getWeight } from '~/services/reception'
|
||||||
import {createWeight, updateWeight} from '~/services/weight'
|
import { createWeight, updateWeight } from '~/services/weight'
|
||||||
|
|
||||||
export type WeighingMode = 'gross' | 'tare'
|
export type WeighingMode = 'gross' | 'tare'
|
||||||
|
|
||||||
@@ -12,16 +12,18 @@ type UseWeighingOptions = {
|
|||||||
reception: Ref<ReceptionData | null>
|
reception: Ref<ReceptionData | null>
|
||||||
updateReception: (id: number, payload: Partial<ReceptionData>) => Promise<ReceptionData | null>
|
updateReception: (id: number, payload: Partial<ReceptionData>) => Promise<ReceptionData | null>
|
||||||
loadReception?: (id: number) => Promise<ReceptionData | null>
|
loadReception?: (id: number) => Promise<ReceptionData | null>
|
||||||
|
storeError?: Ref<string | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWeighing = ({
|
export const useWeighing = ({
|
||||||
mode,
|
mode,
|
||||||
reception,
|
reception,
|
||||||
updateReception,
|
updateReception,
|
||||||
loadReception
|
loadReception,
|
||||||
|
storeError
|
||||||
}: UseWeighingOptions) => {
|
}: UseWeighingOptions) => {
|
||||||
const weightData = ref<WeightData | null>(null)
|
const weightData = ref<WeightData | null>(null)
|
||||||
const isFetching = ref(false)
|
const localErrorMessage = ref<string | null>(null)
|
||||||
|
|
||||||
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
||||||
const weights = reception.value?.weights ?? []
|
const weights = reception.value?.weights ?? []
|
||||||
@@ -31,19 +33,22 @@ export const useWeighing = ({
|
|||||||
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
||||||
const showLoadingBox = computed(
|
const errorMessage = computed(() => localErrorMessage.value ?? storeError?.value ?? null)
|
||||||
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
const showLoadingBox = computed(() => displayWeight.value === null && !errorMessage.value)
|
||||||
)
|
|
||||||
|
|
||||||
const fetchWeight = async () => {
|
const fetchWeight = async () => {
|
||||||
isFetching.value = true
|
localErrorMessage.value = null
|
||||||
weightData.value = await getWeight().finally(() => {
|
try {
|
||||||
isFetching.value = false
|
weightData.value = await getWeight()
|
||||||
})
|
} catch (error) {
|
||||||
|
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveWeight = async () => {
|
const saveWeight = async () => {
|
||||||
|
localErrorMessage.value = null
|
||||||
if (!reception.value) {
|
if (!reception.value) {
|
||||||
|
localErrorMessage.value = 'Réception introuvable.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,32 +58,36 @@ export const useWeighing = ({
|
|||||||
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||||
|
|
||||||
if (baseWeight === null) {
|
if (baseWeight === null) {
|
||||||
|
localErrorMessage.value = 'Veuillez d’abord peser.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingEntry?.id) {
|
try {
|
||||||
await updateWeight(existingEntry.id, {
|
if (existingEntry?.id) {
|
||||||
type: mode,
|
await updateWeight(existingEntry.id, {
|
||||||
dsd: baseDsd,
|
type: mode,
|
||||||
weight: baseWeight,
|
dsd: baseDsd,
|
||||||
weighedAt: baseWeighedAt
|
weight: baseWeight,
|
||||||
})
|
weighedAt: baseWeighedAt
|
||||||
} else {
|
})
|
||||||
await createWeight({
|
} else {
|
||||||
reception: `api/receptions/${reception.value.id}`,
|
await createWeight({
|
||||||
type: mode,
|
reception: `/receptions/${reception.value.id}`,
|
||||||
dsd: baseDsd,
|
type: mode,
|
||||||
weight: baseWeight,
|
dsd: baseDsd,
|
||||||
weighedAt: baseWeighedAt
|
weight: baseWeight,
|
||||||
})
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = mode === 'tare'
|
const nextStep = reception.value.currentStep + 1
|
||||||
? reception.value.currentStep
|
|
||||||
: reception.value.currentStep + 1
|
|
||||||
await updateReception(reception.value.id, {
|
await updateReception(reception.value.id, {
|
||||||
currentStep: nextStep,
|
currentStep: nextStep,
|
||||||
isValid: reception.value.isValid
|
isValid: mode === 'tare' ? true : reception.value.isValid
|
||||||
})
|
})
|
||||||
|
|
||||||
if (loadReception) {
|
if (loadReception) {
|
||||||
@@ -92,6 +101,7 @@ export const useWeighing = ({
|
|||||||
displayWeight,
|
displayWeight,
|
||||||
displayDsd,
|
displayDsd,
|
||||||
title,
|
title,
|
||||||
|
errorMessage,
|
||||||
showLoadingBox,
|
showLoadingBox,
|
||||||
fetchWeight,
|
fetchWeight,
|
||||||
saveWeight
|
saveWeight
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"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.",
|
|
||||||
"weigh": "Impossible de récupérer la pesée."
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"login": "Identifiants invalides.",
|
|
||||||
"users": "Impossible de récupérer les utilisateurs.",
|
|
||||||
"logout": "Impossible de se déconnecter."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"reception": {
|
|
||||||
"update": "Réception mise à jour avec succès."
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"login": "Connexion réussie.",
|
|
||||||
"logout": "Déconnexion réussie."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-screen bg-primary-500 from-primary-50 via-white to-neutral-100 text-neutral-900">
|
|
||||||
<main class="mx-auto flex min-h-screen w-full max-w-[720px] items-center px-6 py-12">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
LOGO
|
LOGO
|
||||||
</span>
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<nav class="mx-8 flex flex-1 gap-8 text-2xl font-bold uppercase text-white">
|
<nav class="mx-8 flex gap-8 text-2xl font-bold uppercase text-white">
|
||||||
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
|
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@@ -29,13 +29,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ml-auto text-xl font-bold uppercase text-white transition hover:opacity-80"
|
|
||||||
@click="handleLogout"
|
|
||||||
>
|
|
||||||
Déconnexion
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="mx-auto w-full max-w-[1050px] px-6 pt-[90px] pb-0">
|
<main class="mx-auto w-full max-w-[1050px] px-6 pt-[90px] pb-0">
|
||||||
@@ -45,17 +38,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const auth = useAuthStore()
|
|
||||||
const isReceptionActive = computed(() => route.path.startsWith('/reception'))
|
const isReceptionActive = computed(() => route.path.startsWith('/reception'))
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await auth.logout()
|
|
||||||
} finally {
|
|
||||||
await navigateTo('/login')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
export default defineNuxtRouteMiddleware(async (to) => {
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
if (to.path === '/login') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!auth.isAuthenticated) {
|
|
||||||
await auth.ensureSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!auth.isAuthenticated) {
|
|
||||||
return navigateTo('/login')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -2,37 +2,13 @@ export default defineNuxtConfig({
|
|||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
ssr: false,
|
ssr: false,
|
||||||
app: {
|
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
||||||
baseURL: process.env.NUXT_PUBLIC_APP_BASE || '/'
|
|
||||||
},
|
|
||||||
modules: [
|
|
||||||
'@nuxtjs/tailwindcss',
|
|
||||||
'@pinia/nuxt',
|
|
||||||
'nuxt-toast',
|
|
||||||
'@nuxtjs/i18n'
|
|
||||||
],
|
|
||||||
css: ['~/assets/css/toast.css'],
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toast: {
|
|
||||||
settings: {
|
|
||||||
timeout: 0,
|
|
||||||
closeOnClick: true,
|
|
||||||
progressBar: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
i18n: {
|
|
||||||
strategy: 'no_prefix',
|
|
||||||
defaultLocale: 'fr',
|
|
||||||
langDir: 'locales',
|
|
||||||
locales: [
|
|
||||||
{ code: 'fr', file: 'fr.json', name: 'Français' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
typescript: {
|
typescript: {
|
||||||
strict: true
|
strict: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
2419
frontend/package-lock.json
generated
2419
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,12 +11,8 @@
|
|||||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/i18n": "^10.2.1",
|
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"izitoast": "^1.4.0",
|
|
||||||
"maska": "^3.2.0",
|
|
||||||
"nuxt": "^4.2.2",
|
"nuxt": "^4.2.2",
|
||||||
"nuxt-toast": "^1.4.0",
|
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mx-auto w-full max-w-lg">
|
|
||||||
<span
|
|
||||||
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
|
||||||
>
|
|
||||||
LOGO
|
|
||||||
</span>
|
|
||||||
<form
|
|
||||||
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
|
||||||
@submit.prevent="handleSubmit"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-semibold text-neutral-700" for="user-select">
|
|
||||||
Utilisateur
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="user-select"
|
|
||||||
v-model="selectedUsername"
|
|
||||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
|
||||||
:disabled="isLoadingUsers"
|
|
||||||
>
|
|
||||||
<option value="" disabled>Choisir un utilisateur</option>
|
|
||||||
<option v-for="user in users" :key="user.username" :value="user.username">
|
|
||||||
{{ user.username }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-semibold text-neutral-700" for="password">
|
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
>
|
|
||||||
Connexion
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { UserData } from '~/services/dto/user-data'
|
|
||||||
import { getUsers } from '~/services/auth'
|
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'auth'
|
|
||||||
})
|
|
||||||
|
|
||||||
const users = ref<UserData[]>([])
|
|
||||||
const isLoadingUsers = ref(true)
|
|
||||||
const selectedUsername = ref('')
|
|
||||||
const password = ref('')
|
|
||||||
|
|
||||||
const isSubmitting = computed(() => {
|
|
||||||
return auth.isLoading || !selectedUsername.value || !password.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadUsers = async () => {
|
|
||||||
isLoadingUsers.value = true
|
|
||||||
try {
|
|
||||||
users.value = await getUsers()
|
|
||||||
} finally {
|
|
||||||
isLoadingUsers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (isSubmitting.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await auth.login(selectedUsername.value, password.value)
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void loadUsers()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="errorMessage" class="text-red-600">{{ errorMessage }}</div>
|
||||||
|
<div v-else>
|
||||||
<div class="flex justify-between h-[52px] mb-[90px]">
|
<div class="flex justify-between h-[52px] mb-[90px]">
|
||||||
<p class="self-center">Indicateur d’étapes</p>
|
<p class="self-center">Indicateur d’étapes</p>
|
||||||
<button
|
<NuxtLink to="/" class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center">Mettre en attente</NuxtLink>
|
||||||
type="button"
|
|
||||||
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
|
||||||
@click="saveAndHold"
|
|
||||||
>Mettre en attente</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
<ReceptionForm v-if="storeReception?.currentStep === 0"/>
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
||||||
<ReceptionUnloading v-if="storeReception?.currentStep === 2"/>
|
<ReceptionUnloading v-if="storeReception?.currentStep === 2"/>
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
<ReceptionWeight v-if="storeReception?.currentStep === 3" mode="tare"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,41 +20,17 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const { current: storeReception } = storeToRefs(receptionStore)
|
const { current: storeReception, errorMessage } = storeToRefs(receptionStore)
|
||||||
|
|
||||||
const resolveReceptionId = (param: unknown) => {
|
onMounted(async () => {
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
const raw = route.params.id
|
||||||
if (!idStr) {
|
const idStr = Array.isArray(raw) ? raw[0] : raw
|
||||||
return null
|
const id = idStr ? Number(idStr) : null
|
||||||
}
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
if (id === null) {
|
||||||
() => route.params.id,
|
await receptionStore.createReception()
|
||||||
async (param) => {
|
} else {
|
||||||
const id = resolveReceptionId(param)
|
|
||||||
if (id === null) {
|
|
||||||
receptionStore.clearCurrent()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await receptionStore.loadReception(id)
|
await receptionStore.loadReception(id)
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const saveAndHold = async () => {
|
|
||||||
if (!receptionStore.current) {
|
|
||||||
await router.push('/')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
currentStep: receptionStore.current.currentStep,
|
|
||||||
licensePlate: receptionStore.current.licensePlate,
|
|
||||||
receptionDate: receptionStore.current.receptionDate
|
|
||||||
})
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
|
||||||
import type { UserData } from '~/services/dto/user-data'
|
|
||||||
|
|
||||||
export async function getUsers() {
|
|
||||||
const api = useApi()
|
|
||||||
const data = await api.get<UserData[] | { 'hydra:member': UserData[] }>('users', {}, {
|
|
||||||
toastErrorKey: 'errors.auth.users'
|
|
||||||
})
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
return data['hydra:member'] ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCurrentUser() {
|
|
||||||
const api = useApi()
|
|
||||||
return api.get<UserData>('me', {}, {
|
|
||||||
toast: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function login(username: string, password: string) {
|
|
||||||
const api = useApi()
|
|
||||||
return api.post<{ token: string }>('login_check', { username, password }, {
|
|
||||||
toastErrorKey: 'errors.auth.login',
|
|
||||||
toastSuccessKey: 'success.auth.login'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function logout() {
|
|
||||||
const api = useApi()
|
|
||||||
return api.post<void>('logout', {}, {
|
|
||||||
toastErrorKey: 'errors.auth.logout',
|
|
||||||
toastSuccessKey: 'success.auth.logout',
|
|
||||||
redirect: 'manual'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export interface UserData {
|
|
||||||
username: string
|
|
||||||
}
|
|
||||||
@@ -1,39 +1,50 @@
|
|||||||
import {useApi} from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type {ReceptionData} from '~/services/dto/reception-data'
|
import type { ReceptionData } from '~/services/dto/reception-data'
|
||||||
import type {WeightData} from '~/services/dto/weight-data'
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
export async function getReceptionList() {
|
export async function getReceptionList() {
|
||||||
const api = useApi()
|
try {
|
||||||
return api.get<ReceptionData>(`receptions`, {}, {
|
return await api.get<ReceptionData>(`receptions`)
|
||||||
toastErrorKey: 'errors.reception.list'
|
} catch (error) {
|
||||||
})
|
console.error(error.message, error)
|
||||||
|
return error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReception(id: number) {
|
export async function getReception(id: number) {
|
||||||
const api = useApi()
|
try {
|
||||||
return api.get<ReceptionData>(`receptions/${id}`, {}, {
|
return await api.get<ReceptionData>(`receptions/${id}`)
|
||||||
toastErrorKey: 'errors.reception.fetch'
|
} catch (error) {
|
||||||
})
|
console.error(error.message, error)
|
||||||
|
return error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createReception(payload: Partial<ReceptionData> = {}) {
|
export async function createReception(payload: Partial<ReceptionData> = {}) {
|
||||||
const api = useApi()
|
try {
|
||||||
return api.post<ReceptionData>('receptions', payload, {
|
return await api.post<ReceptionData>('receptions', payload)
|
||||||
toastErrorKey: 'errors.reception.create'
|
} catch (error) {
|
||||||
})
|
console.error(error.message, error)
|
||||||
|
return error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateReception(id: number, payload: Partial<ReceptionData>) {
|
export async function updateReception(id: number, payload: Partial<ReceptionData>) {
|
||||||
const api = useApi()
|
try {
|
||||||
return api.patch<ReceptionData>(`receptions/${id}`, payload, {
|
return await api.patch<ReceptionData>(`receptions/${id}`, payload)
|
||||||
toastErrorKey: 'errors.reception.update',
|
} catch (error) {
|
||||||
toastSuccessKey: 'success.reception.update'
|
console.error(error.message, error)
|
||||||
})
|
return error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWeight(): Promise<WeightData> {
|
export async function getWeight(): Promise<WeightData> {
|
||||||
const api = useApi()
|
try {
|
||||||
return api.get<WeightData>('receptions/weigh', {}, {
|
return await api.get<WeightData>('receptions/weigh')
|
||||||
toastErrorKey: 'errors.reception.weigh'
|
} catch (error) {
|
||||||
})
|
console.error(error.message, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type { WeightEntryData } from '~/services/dto/reception-data'
|
import type { WeightEntryData } from '~/services/dto/reception-data'
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
export type WeightPayload = {
|
export type WeightPayload = {
|
||||||
reception: string
|
reception: string
|
||||||
type: 'gross' | 'tare'
|
type: 'gross' | 'tare'
|
||||||
@@ -10,11 +12,19 @@ export type WeightPayload = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createWeight(payload: WeightPayload) {
|
export async function createWeight(payload: WeightPayload) {
|
||||||
const api = useApi()
|
try {
|
||||||
return api.post<WeightEntryData>('weights', payload)
|
return await api.post<WeightEntryData>('weights', payload)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateWeight(id: number, payload: Partial<WeightPayload>) {
|
export async function updateWeight(id: number, payload: Partial<WeightPayload>) {
|
||||||
const api = useApi()
|
try {
|
||||||
return api.patch<WeightEntryData>(`weights/${id}`, payload)
|
return await api.patch<WeightEntryData>(`weights/${id}`, payload)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import type { UserData } from '~/services/dto/user-data'
|
|
||||||
import { getCurrentUser, login, logout } from '~/services/auth'
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
|
||||||
state: () => ({
|
|
||||||
user: null as UserData | null,
|
|
||||||
isLoading: false,
|
|
||||||
checked: false
|
|
||||||
}),
|
|
||||||
getters: {
|
|
||||||
isAuthenticated: (state) => Boolean(state.user)
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
async ensureSession() {
|
|
||||||
if (this.checked) {
|
|
||||||
return this.user
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checked = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const me = await getCurrentUser()
|
|
||||||
this.user = me
|
|
||||||
return me
|
|
||||||
} catch {
|
|
||||||
this.user = null
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async login(username: string, password: string) {
|
|
||||||
this.isLoading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
await login(username, password)
|
|
||||||
const me = await getCurrentUser()
|
|
||||||
this.user = me
|
|
||||||
this.checked = true
|
|
||||||
return me
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async logout() {
|
|
||||||
this.isLoading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
await logout()
|
|
||||||
} catch {
|
|
||||||
// Ignore logout errors so we can still clear local auth state.
|
|
||||||
} finally {
|
|
||||||
this.user = null
|
|
||||||
this.checked = true
|
|
||||||
this.isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -9,51 +9,64 @@ const isReceptionData = (value: unknown): value is ReceptionData => {
|
|||||||
export const useReceptionStore = defineStore('reception', {
|
export const useReceptionStore = defineStore('reception', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
current: null as ReceptionData | null,
|
current: null as ReceptionData | null,
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
errorMessage: null as string | null
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setCurrent(reception: ReceptionData | null) {
|
setCurrent(reception: ReceptionData | null) {
|
||||||
this.current = reception
|
this.current = reception
|
||||||
},
|
},
|
||||||
clearCurrent() {
|
clearError() {
|
||||||
this.current = null
|
this.errorMessage = null
|
||||||
},
|
},
|
||||||
async loadReception(id: number) {
|
async loadReception(id: number) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const result = await getReception(id).finally(() => {
|
this.errorMessage = null
|
||||||
this.isLoading = false
|
try {
|
||||||
})
|
const result = await getReception(id)
|
||||||
if (!isReceptionData(result)) {
|
if (!isReceptionData(result)) {
|
||||||
this.current = null
|
this.errorMessage = 'Réception introuvable.'
|
||||||
return null
|
this.current = null
|
||||||
}
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
this.current = result
|
this.current = result
|
||||||
return result
|
return result
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async createReception(payload: Partial<ReceptionData> = {}) {
|
async createReception() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const result = await createReception(payload).finally(() => {
|
this.errorMessage = null
|
||||||
this.isLoading = false
|
try {
|
||||||
})
|
const result = await createReception()
|
||||||
if (!isReceptionData(result)) {
|
if (!isReceptionData(result)) {
|
||||||
return null
|
this.errorMessage = 'Impossible de créer la réception.'
|
||||||
}
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
this.current = result
|
this.current = result
|
||||||
return result
|
return result
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async updateReception(id: number, payload: Partial<ReceptionData>) {
|
async updateReception(id: number, payload: Partial<ReceptionData>) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const result = await updateReception(id, payload).finally(() => {
|
this.errorMessage = null
|
||||||
this.isLoading = false
|
try {
|
||||||
})
|
const result = await updateReception(id, payload)
|
||||||
if (!isReceptionData(result)) {
|
if (!isReceptionData(result)) {
|
||||||
return null
|
this.errorMessage = 'Impossible de mettre à jour la réception.'
|
||||||
}
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
this.current = result
|
this.current = result
|
||||||
return result
|
return result
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
11
makefile
11
makefile
@@ -7,7 +7,7 @@ ENV_FILE := $(if $(wildcard $(ENV_LOCAL)),$(ENV_LOCAL),$(ENV_DEFAULT))
|
|||||||
include $(ENV_DEFAULT)
|
include $(ENV_DEFAULT)
|
||||||
-include $(ENV_LOCAL)
|
-include $(ENV_LOCAL)
|
||||||
|
|
||||||
PHP_CONTAINER = php-$(DOCKER_APP_NAME)-fpm
|
PHP_CONTAINER = php-$(DOCKER_APP_NAME)-apache
|
||||||
SYMFONY_CONSOLE = $(EXEC_PHP) php bin/console
|
SYMFONY_CONSOLE = $(EXEC_PHP) php bin/console
|
||||||
|
|
||||||
DOCKER_COMPOSE = docker compose --env-file $(ENV_FILE)
|
DOCKER_COMPOSE = docker compose --env-file $(ENV_FILE)
|
||||||
@@ -40,14 +40,13 @@ restart: env-init
|
|||||||
$(DOCKER_COMPOSE) down
|
$(DOCKER_COMPOSE) down
|
||||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS migration-migrate
|
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS
|
||||||
|
|
||||||
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
||||||
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
||||||
|
|
||||||
composer-install:
|
composer-install:
|
||||||
$(EXEC_PHP) composer install
|
$(EXEC_PHP) composer install
|
||||||
$(SYMFONY_CONSOLE) lexik:jwt:generate-keypair --skip-if-exists
|
|
||||||
|
|
||||||
build-nuxtJS:
|
build-nuxtJS:
|
||||||
# $(EXEC_PHP) cp -n frontend/.env.dist frontend/.env.local
|
# $(EXEC_PHP) cp -n frontend/.env.dist frontend/.env.local
|
||||||
@@ -73,16 +72,10 @@ build-without-cache:
|
|||||||
--build-arg="CURRENT_GID=$(shell id -g)" \
|
--build-arg="CURRENT_GID=$(shell id -g)" \
|
||||||
--no-cache
|
--no-cache
|
||||||
|
|
||||||
migration-migrate:
|
|
||||||
$(SYMFONY_CONSOLE) bin/console doctrine:migrations:migrate --no-interaction
|
|
||||||
|
|
||||||
# Attention, supprime votre bdd local
|
# Attention, supprime votre bdd local
|
||||||
db-reset:
|
db-reset:
|
||||||
$(DOCKER_COMPOSE) down -v
|
$(DOCKER_COMPOSE) down -v
|
||||||
$(DOCKER_COMPOSE) up -d
|
$(DOCKER_COMPOSE) up -d
|
||||||
$(MAKE) wait
|
|
||||||
$(SYMFONY_CONSOLE) doctrine:database:create --if-not-exists
|
|
||||||
$(MAKE) migration-migrate
|
|
||||||
|
|
||||||
# Restart la bdd
|
# Restart la bdd
|
||||||
db-restart:
|
db-restart:
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260112000700 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Create user table for authentication';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE "user" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_USER_USERNAME ON "user" (username)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('DROP TABLE "user"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260112000800 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Ensure user table exists and align weight type default';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE IF NOT EXISTS "user" (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS UNIQ_8D93D649F85E0677 ON "user" (username)');
|
|
||||||
$this->addSql('ALTER TABLE weight ALTER type DROP DEFAULT');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE weight ALTER type SET DEFAULT \'gross\'');
|
|
||||||
$this->addSql('DROP INDEX IF EXISTS UNIQ_8D93D649F85E0677');
|
|
||||||
$this->addSql('DROP TABLE IF EXISTS "user"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
|
|
||||||
BRANCH="develop"
|
|
||||||
|
|
||||||
for cmd in git php composer npm; do
|
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
echo "Missing required command: $cmd" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "==> Pulling latest code ($BRANCH)"
|
|
||||||
git fetch origin "$BRANCH"
|
|
||||||
git checkout "$BRANCH"
|
|
||||||
git pull --ff-only origin "$BRANCH"
|
|
||||||
|
|
||||||
echo "==> Installing backend deps (prod)"
|
|
||||||
composer install --no-dev --optimize-autoloader
|
|
||||||
|
|
||||||
echo "==> Running DB migrations"
|
|
||||||
php bin/console doctrine:migrations:migrate --no-interaction --env=prod
|
|
||||||
|
|
||||||
echo "==> Warming Symfony cache (prod)"
|
|
||||||
php bin/console cache:clear --env=prod
|
|
||||||
php bin/console cache:warmup --env=prod
|
|
||||||
|
|
||||||
echo "==> Building frontend (static)"
|
|
||||||
cd "$ROOT_DIR/frontend"
|
|
||||||
npm ci
|
|
||||||
npm run generate
|
|
||||||
|
|
||||||
echo "==> Done."
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Usage: ./scripts/deploy-release.sh v0.0.1
|
|
||||||
# Requires: curl, tar, (optional) rsync
|
|
||||||
#
|
|
||||||
# Auth token: set RELEASE_TOKEN env var or create /etc/ferme-release-token
|
|
||||||
|
|
||||||
TAG="${1:-}"
|
|
||||||
if [ -z "$TAG" ]; then
|
|
||||||
echo "Usage: $0 v0.0.1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPO_OWNER="MALIO-DEV"
|
|
||||||
REPO_NAME="Ferme"
|
|
||||||
GITEA_API="https://gitea.malio.fr/api/v1"
|
|
||||||
DEPLOY_DIR="/var/www/ferme"
|
|
||||||
|
|
||||||
if [ -f /etc/ferme-release-token ] && [ -z "${RELEASE_TOKEN:-}" ]; then
|
|
||||||
RELEASE_TOKEN="$(cat /etc/ferme-release-token)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmp_dir="$(mktemp -d)"
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$tmp_dir"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
release_json="$tmp_dir/release.json"
|
|
||||||
curl_opts=(-sS)
|
|
||||||
if [ -n "${RELEASE_TOKEN:-}" ]; then
|
|
||||||
curl_opts+=(-H "Authorization: token ${RELEASE_TOKEN}")
|
|
||||||
fi
|
|
||||||
curl "${curl_opts[@]}" \
|
|
||||||
"${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${TAG}" \
|
|
||||||
-o "$release_json"
|
|
||||||
|
|
||||||
asset_url="$(python3 - "$release_json" <<'PY'
|
|
||||||
import json, sys
|
|
||||||
data = json.load(open(sys.argv[1], 'r'))
|
|
||||||
assets = data.get("assets", [])
|
|
||||||
for a in assets:
|
|
||||||
name = a.get("name", "")
|
|
||||||
if name.startswith("ferme-") and name.endswith(".tar.gz"):
|
|
||||||
print(a.get("browser_download_url", ""))
|
|
||||||
break
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [ -z "$asset_url" ]; then
|
|
||||||
echo "Release asset not found for tag ${TAG}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
archive="$tmp_dir/artefact.tar.gz"
|
|
||||||
curl "${curl_opts[@]}" -L "$asset_url" -o "$archive"
|
|
||||||
|
|
||||||
tar -xzf "$archive" -C "$tmp_dir"
|
|
||||||
|
|
||||||
if command -v rsync >/dev/null 2>&1; then
|
|
||||||
rsync -a --delete \
|
|
||||||
--exclude ".env" \
|
|
||||||
--exclude ".env.local" \
|
|
||||||
--exclude "config/jwt" \
|
|
||||||
--exclude "var" \
|
|
||||||
"$tmp_dir"/ "$DEPLOY_DIR"/
|
|
||||||
else
|
|
||||||
cp -a "$tmp_dir"/. "$DEPLOY_DIR"/
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Release ${TAG} deployed to ${DEPLOY_DIR}"
|
|
||||||
|
|
||||||
if [ -n "${DEPLOY_OWNER:-}" ]; then
|
|
||||||
DEPLOY_GROUP="${DEPLOY_GROUP:-www-data}"
|
|
||||||
chown -R "${DEPLOY_OWNER}:${DEPLOY_GROUP}" "$DEPLOY_DIR"
|
|
||||||
chmod -R g+rx,o+rx "$DEPLOY_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${DEPLOY_DIR}/.env.local" ]; then
|
|
||||||
echo "Running migrations (if any)..."
|
|
||||||
php "${DEPLOY_DIR}/bin/console" doctrine:migrations:migrate --no-interaction --env=prod
|
|
||||||
else
|
|
||||||
echo "Skip migrations: ${DEPLOY_DIR}/.env.local not found" >&2
|
|
||||||
fi
|
|
||||||
@@ -11,7 +11,6 @@ use ApiPlatform\Metadata\Patch;
|
|||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
use App\Dto\PontBasculeReading;
|
use App\Dto\PontBasculeReading;
|
||||||
use App\State\ReceptionReceiptProvider;
|
|
||||||
use App\State\ReceptionWeighingProvider;
|
use App\State\ReceptionWeighingProvider;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
@@ -52,18 +51,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
output: PontBasculeReading::class,
|
output: PontBasculeReading::class,
|
||||||
provider: ReceptionWeighingProvider::class,
|
provider: ReceptionWeighingProvider::class,
|
||||||
),
|
),
|
||||||
new Get(
|
|
||||||
uriTemplate: '/receptions/{id}/receipt',
|
|
||||||
requirements: ['id' => '\d+'],
|
|
||||||
openapi: new OpenApiOperation(
|
|
||||||
summary: 'Render a reception receipt',
|
|
||||||
description: 'Returns a PDF receipt for the reception.',
|
|
||||||
),
|
|
||||||
output: false,
|
|
||||||
provider: ReceptionReceiptProvider::class,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_USER')",
|
|
||||||
)]
|
)]
|
||||||
class Reception
|
class Reception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Entity;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
|
||||||
use ApiPlatform\Metadata\Get;
|
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
|
||||||
use App\State\MeProvider;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
|
||||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
|
||||||
|
|
||||||
#[ORM\Entity]
|
|
||||||
#[ORM\Table(name: 'user', schema: 'public')]
|
|
||||||
#[ApiResource(
|
|
||||||
operations: [
|
|
||||||
new Get(
|
|
||||||
uriTemplate: '/me',
|
|
||||||
normalizationContext: ['groups' => ['user:read']],
|
|
||||||
security: "is_granted('ROLE_USER')",
|
|
||||||
provider: MeProvider::class
|
|
||||||
),
|
|
||||||
new GetCollection(
|
|
||||||
normalizationContext: ['groups' => ['user:read']],
|
|
||||||
security: "is_granted('PUBLIC_ACCESS')"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['user:read']],
|
|
||||||
paginationEnabled: false
|
|
||||||
)]
|
|
||||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|
||||||
{
|
|
||||||
#[ORM\Id]
|
|
||||||
#[ORM\GeneratedValue]
|
|
||||||
#[ORM\Column(type: 'integer')]
|
|
||||||
private ?int $id = null;
|
|
||||||
|
|
||||||
#[ORM\Column(length: 180, unique: true)]
|
|
||||||
#[Groups(['user:read'])]
|
|
||||||
private string $username = '';
|
|
||||||
|
|
||||||
#[ORM\Column(type: 'json')]
|
|
||||||
private array $roles = [];
|
|
||||||
|
|
||||||
#[ORM\Column]
|
|
||||||
private string $password = '';
|
|
||||||
|
|
||||||
public function getId(): ?int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUsername(): string
|
|
||||||
{
|
|
||||||
return $this->username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setUsername(string $username): self
|
|
||||||
{
|
|
||||||
$this->username = $username;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUserIdentifier(): string
|
|
||||||
{
|
|
||||||
return $this->username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRoles(): array
|
|
||||||
{
|
|
||||||
$roles = $this->roles;
|
|
||||||
$roles[] = 'ROLE_USER';
|
|
||||||
|
|
||||||
return array_unique($roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setRoles(array $roles): self
|
|
||||||
{
|
|
||||||
$this->roles = $roles;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPassword(): string
|
|
||||||
{
|
|
||||||
return $this->password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setPassword(string $password): self
|
|
||||||
{
|
|
||||||
$this->password = $password;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function eraseCredentials(): void
|
|
||||||
{
|
|
||||||
// No-op: we don't store temporary sensitive data on the entity.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,6 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
denormalizationContext: ['groups' => ['weight:write']],
|
denormalizationContext: ['groups' => ['weight:write']],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_USER')",
|
|
||||||
)]
|
)]
|
||||||
#[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')]
|
#[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')]
|
||||||
class Weight
|
class Weight
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\State;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\Operation;
|
|
||||||
use ApiPlatform\State\ProviderInterface;
|
|
||||||
use App\Entity\User;
|
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
|
||||||
use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
|
|
||||||
|
|
||||||
final readonly class MeProvider implements ProviderInterface
|
|
||||||
{
|
|
||||||
public function __construct(private Security $security) {}
|
|
||||||
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
|
|
||||||
{
|
|
||||||
$user = $this->security->getUser();
|
|
||||||
|
|
||||||
if (!$user instanceof User) {
|
|
||||||
throw new AccessDeniedException('User not authenticated.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\State;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\Operation;
|
|
||||||
use ApiPlatform\State\ProviderInterface;
|
|
||||||
use App\Entity\Reception;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Dompdf\Dompdf;
|
|
||||||
use Dompdf\Options;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
||||||
use Twig\Environment;
|
|
||||||
use Twig\Error\LoaderError;
|
|
||||||
use Twig\Error\RuntimeError;
|
|
||||||
use Twig\Error\SyntaxError;
|
|
||||||
|
|
||||||
final readonly class ReceptionReceiptProvider implements ProviderInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private Environment $twig,
|
|
||||||
private EntityManagerInterface $entityManager,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws RuntimeError
|
|
||||||
* @throws SyntaxError
|
|
||||||
* @throws LoaderError
|
|
||||||
*/
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
|
||||||
{
|
|
||||||
$id = $uriVariables['id'] ?? null;
|
|
||||||
if (null === $id) {
|
|
||||||
throw new NotFoundHttpException('Reception not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$reception = $this->entityManager->getRepository(Reception::class)->find($id);
|
|
||||||
if (!$reception instanceof Reception) {
|
|
||||||
throw new NotFoundHttpException('Reception not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$options = new Options();
|
|
||||||
$options->set('isRemoteEnabled', true);
|
|
||||||
|
|
||||||
$dompdf = new Dompdf($options);
|
|
||||||
$html = $this->twig->render('reception_voucher.html.twig', [
|
|
||||||
'reception' => $reception,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$dompdf->loadHtml($html);
|
|
||||||
$dompdf->setPaper('A4');
|
|
||||||
$dompdf->render();
|
|
||||||
|
|
||||||
$filename = sprintf('bon-reception-%d.pdf', $reception->getId());
|
|
||||||
|
|
||||||
return new Response($dompdf->output(), Response::HTTP_OK, [
|
|
||||||
'Content-Type' => 'application/pdf',
|
|
||||||
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
symfony.lock
12
symfony.lock
@@ -61,18 +61,6 @@
|
|||||||
".php-cs-fixer.dist.php"
|
".php-cs-fixer.dist.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lexik/jwt-authentication-bundle": {
|
|
||||||
"version": "3.2",
|
|
||||||
"recipe": {
|
|
||||||
"repo": "github.com/symfony/recipes",
|
|
||||||
"branch": "main",
|
|
||||||
"version": "2.5",
|
|
||||||
"ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"config/packages/lexik_jwt_authentication.yaml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nelmio/cors-bundle": {
|
"nelmio/cors-bundle": {
|
||||||
"version": "2.6",
|
"version": "2.6",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@page { margin: 56px 56px; }
|
|
||||||
|
|
||||||
body{
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
margin:0;
|
|
||||||
color:#000;
|
|
||||||
}
|
|
||||||
|
|
||||||
p{ margin:0; }
|
|
||||||
em{ font-style: normal; }
|
|
||||||
|
|
||||||
.red{ color:red; }
|
|
||||||
|
|
||||||
.company-block{
|
|
||||||
font-size:13px;
|
|
||||||
text-align:left;
|
|
||||||
line-height:1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box{
|
|
||||||
border:1px solid #000;
|
|
||||||
border-radius:10px;
|
|
||||||
padding:10px;
|
|
||||||
font-size:13px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title{
|
|
||||||
text-align:center;
|
|
||||||
font-size: 18pt;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 4mm 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table{ width:100%; border-collapse: collapse; }
|
|
||||||
th, td{
|
|
||||||
border:1px solid #333;
|
|
||||||
padding:4px 6px;
|
|
||||||
vertical-align: top;
|
|
||||||
font-size: 9pt;
|
|
||||||
}
|
|
||||||
th{ text-align:center; font-weight:700; }
|
|
||||||
|
|
||||||
/* tables de layout (sans bordures) */
|
|
||||||
.layout, .layout td{ border:none !important; padding:0; }
|
|
||||||
|
|
||||||
/* GRAND TABLEAU : verrouillage dompdf */
|
|
||||||
.bigtable{ table-layout: fixed; }
|
|
||||||
|
|
||||||
/* ligne “filler” pour forcer la hauteur comme l'exemple */
|
|
||||||
.fill td{
|
|
||||||
border-top:none;
|
|
||||||
height: 75mm; /* ajuste si besoin */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bloc IDTF comme l’exemple */
|
|
||||||
table.idtf{
|
|
||||||
width: 350px; /* ou 100% si tu préfères */
|
|
||||||
border-collapse: collapse;
|
|
||||||
border: 1px solid #333 !important; /* bordure extérieure */
|
|
||||||
margin-top: 12px; /* ~3mm */
|
|
||||||
font-size: 8.5pt;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IMPORTANT: on cible td DANS table.idtf et on force */
|
|
||||||
table.idtf td{
|
|
||||||
border: 1px solid #333 !important;
|
|
||||||
padding: 3px 5px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Largeurs en % (pas de mm) */
|
|
||||||
table.idtf td.n{ width: 8%; text-align:center; }
|
|
||||||
table.idtf td.prod{ width: 52%; }
|
|
||||||
table.idtf td.net{ width: 20%; }
|
|
||||||
table.idtf td.date{ width: 20%; }
|
|
||||||
|
|
||||||
.signature-title{ font-size:9pt; margin-bottom:2mm; }
|
|
||||||
.signature-box{ height: 22mm; margin-bottom: 4mm; }
|
|
||||||
.meta{ font-size: 9pt; line-height: 1.35; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- HEADER -->
|
|
||||||
<table class="layout" style="width:100%;">
|
|
||||||
<tr>
|
|
||||||
<td style="width:70%; vertical-align:top;">
|
|
||||||
<!-- table imbriquée : 1 ligne logo, 1 ligne texte (dompdf-friendly) -->
|
|
||||||
<table class="layout" style="width:100%;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding:0; border:none;">
|
|
||||||
<img src="https://static.mixsuite.fr/liot/logo.png"
|
|
||||||
style="width:110px; display:block; margin:0 0 4mm 0;">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="company-block" style="padding:0; border:none;">
|
|
||||||
<strong>SA LIOT Châtellerault</strong><br>
|
|
||||||
Site de <b>Châtellerault</b><br>
|
|
||||||
14 Allée d’Argenson<br>
|
|
||||||
Z.I Nord – Secteur Est<br>
|
|
||||||
86100 CHATELLERAULT<br>
|
|
||||||
TEL : 05 49 20 09 10 – Fax : 05 49 85 37 82<br>
|
|
||||||
Email : lpc.contacts@lpc-liot.fr<br>
|
|
||||||
RCS Châtellerault B 339 505 612
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td style="width:30%; text-align:left; vertical-align:top;">
|
|
||||||
<div class="box" style="display:inline-block; width:75mm;">
|
|
||||||
<strong class="red">Nom de l'entreprise</strong><br><br><br>
|
|
||||||
<span class="red">Adresse de l'entreprise</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="title">BON DE RECEPTION</div>
|
|
||||||
|
|
||||||
<!-- INFOS (code/date/num) -->
|
|
||||||
<table style="margin-bottom:3mm; width:100%; border-collapse:collapse; table-layout:fixed;">
|
|
||||||
<tr>
|
|
||||||
<th style="width:60%; text-align:center;">Code fournisseur</th>
|
|
||||||
<th style="width:20%; text-align:center; white-space:nowrap;">Date</th>
|
|
||||||
<th style="width:20%; text-align:center; white-space:nowrap;">N° réception</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="red" style="width:60%; text-align:center;">XXX</td>
|
|
||||||
<td style="width:20%; text-align:center; white-space:nowrap;">
|
|
||||||
{{ reception.receptionDate|date('d/m/Y') }}
|
|
||||||
</td>
|
|
||||||
<td class="red" style="width:20%; text-align:center; white-space:nowrap;">86-BR-XXXX</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- GRAND TABLEAU -->
|
|
||||||
<table class="bigtable" style="margin-bottom:10px; width:100%; border-collapse:collapse; table-layout:fixed;">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width:15%; text-align:center;">Code</th>
|
|
||||||
<th style="width:65%; text-align:center;">Désignation</th>
|
|
||||||
<th style="width:20%; text-align:right; white-space:nowrap;">Qté livrée (kg)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="red" style="width:12%;">M</td>
|
|
||||||
|
|
||||||
<td style="width:68%;">
|
|
||||||
<strong class="red">MAÏS sec</strong><br><br>
|
|
||||||
|
|
||||||
<div style="font-size:8.5pt; line-height:1.25;">
|
|
||||||
{% set grossWeight = null %}
|
|
||||||
{% set tareWeight = null %}
|
|
||||||
|
|
||||||
{% for weight in reception.weights %}
|
|
||||||
{% if weight.type == 'gross' %}
|
|
||||||
{% set grossWeight = weight %}
|
|
||||||
<p>Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
|
|
||||||
{% elseif weight.type == 'tare' %}
|
|
||||||
{% set tareWeight = weight %}
|
|
||||||
<p>Poids à vide : {{ tareWeight.weight }}kg (pesée n°{{ tareWeight.dsd }} {{ tareWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="red" style="width:20%; text-align:right; white-space:nowrap;">
|
|
||||||
{% if grossWeight and tareWeight %}
|
|
||||||
{{ grossWeight.weight - tareWeight.weight }}
|
|
||||||
{% else %}
|
|
||||||
0
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- filler : garde le grand bloc haut comme sur l'exemple -->
|
|
||||||
<tr class="fill">
|
|
||||||
<td style="width:15%;"></td>
|
|
||||||
<td style="width:65%;"></td>
|
|
||||||
<td style="width:20%;"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- BAS : meta à gauche / signatures à droite (sans float) -->
|
|
||||||
<table class="layout">
|
|
||||||
<tr>
|
|
||||||
<td style="width:60%; padding-right:8mm; vertical-align:top;">
|
|
||||||
<div class="meta red">
|
|
||||||
Transporteur : <strong class="red">Nom du transporteur</strong><br>
|
|
||||||
Mode de livraison : <strong class="red">Fond-mouvant</strong><br>
|
|
||||||
Immatriculation : <strong class="red">{{ reception.licensePlate }}</strong><br><br>
|
|
||||||
Poids annoncé : <strong class="red">XXXXX kg</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bloc IDTF -->
|
|
||||||
<table class="idtf">
|
|
||||||
<tr>
|
|
||||||
<td class="n">1</td>
|
|
||||||
<td class="prod red">
|
|
||||||
Produit : <span class="red">Nom du produit</span><br>
|
|
||||||
N° IDTF : <span class="red">4000XX</span>
|
|
||||||
</td>
|
|
||||||
<td class="net red">Nettoyage : <span class="red">A</span></td>
|
|
||||||
<td class="date red">Date : <span class="red">14/01/2026</span></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- répète le <tr> si besoin -->
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td style="width:40%; vertical-align:top;">
|
|
||||||
<div class="signature-title">Signature :</div>
|
|
||||||
<div class="box signature-box">Ets Liot :</div>
|
|
||||||
<div class="box signature-box">Transporteur :</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Tests\Service;
|
|
||||||
|
|
||||||
use App\Exception\PontBasculeException;
|
|
||||||
use App\Service\PontBasculePayloadDecoder;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class PontBasculePayloadDecoderTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testDecodeValidPayload(): void
|
|
||||||
{
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
|
|
||||||
$payload = json_encode([
|
|
||||||
'response_ascii' => "\u{0001}\u{0002}040200\u{0002}01001420.kg \u{0002}02000000.kg \u{0002}03001420.kg \u{0002}9900121",
|
|
||||||
], JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
$result = $decoder->decode($payload);
|
|
||||||
|
|
||||||
self::assertSame(121, $result->getDsd());
|
|
||||||
self::assertSame(1420.0, $result->getWeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDecodeInvalidPayloadThrows(): void
|
|
||||||
{
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
|
|
||||||
$this->expectException(PontBasculeException::class);
|
|
||||||
$this->expectExceptionMessage('Réponse invalide du pont bascule.');
|
|
||||||
|
|
||||||
$decoder->decode('not-json');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDecodeMissingFieldThrows(): void
|
|
||||||
{
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
$payload = json_encode(['ok' => true], JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
$this->expectException(PontBasculeException::class);
|
|
||||||
$this->expectExceptionMessage('Réponse incomplète du pont bascule: champ "response_ascii" manquant.');
|
|
||||||
|
|
||||||
$decoder->decode($payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDecodeUnreadableValuesThrows(): void
|
|
||||||
{
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
$payload = json_encode(['response_ascii' => 'no-data'], JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
$this->expectException(PontBasculeException::class);
|
|
||||||
$this->expectExceptionMessage('Impossible de lire les valeurs de pesée du pont bascule.');
|
|
||||||
|
|
||||||
$decoder->decode($payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Tests\Service;
|
|
||||||
|
|
||||||
use App\Exception\PontBasculeException;
|
|
||||||
use App\Service\PontBasculePayloadDecoder;
|
|
||||||
use App\Service\PontBasculeService;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class PontBasculeServiceTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testFetchBypassUsesDecoder(): void
|
|
||||||
{
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
|
|
||||||
$httpClient = $this->createMock(HttpClientInterface::class);
|
|
||||||
$httpClient->expects(self::never())->method('request');
|
|
||||||
|
|
||||||
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', true);
|
|
||||||
|
|
||||||
$result = $service->fetch();
|
|
||||||
|
|
||||||
self::assertSame(121, $result->getDsd());
|
|
||||||
self::assertSame(1420.0, $result->getWeight());
|
|
||||||
self::assertInstanceOf(DateTimeImmutable::class, $result->getWeighedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFetchUsesHttpClientWhenNotBypass(): void
|
|
||||||
{
|
|
||||||
$payload = json_encode([
|
|
||||||
'response_ascii' => "\u{0001}\u{0002}040200\u{0002}03000123.kg \u{0002}9900042",
|
|
||||||
], JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
$response = $this->createMock(ResponseInterface::class);
|
|
||||||
$response->expects(self::once())->method('getContent')->with(false)->willReturn($payload);
|
|
||||||
|
|
||||||
$httpClient = $this->createMock(HttpClientInterface::class);
|
|
||||||
$httpClient
|
|
||||||
->expects(self::once())
|
|
||||||
->method('request')
|
|
||||||
->with('POST', 'http://example.test')
|
|
||||||
->willReturn($response)
|
|
||||||
;
|
|
||||||
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
|
|
||||||
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', false);
|
|
||||||
|
|
||||||
$result = $service->fetch();
|
|
||||||
|
|
||||||
self::assertSame(42, $result->getDsd());
|
|
||||||
self::assertSame(123.0, $result->getWeight());
|
|
||||||
self::assertInstanceOf(DateTimeImmutable::class, $result->getWeighedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFetchThrowsOnTransportFailure(): void
|
|
||||||
{
|
|
||||||
$exception = $this->createStub(TransportExceptionInterface::class);
|
|
||||||
|
|
||||||
$httpClient = $this->createMock(HttpClientInterface::class);
|
|
||||||
$httpClient
|
|
||||||
->expects(self::once())
|
|
||||||
->method('request')
|
|
||||||
->willThrowException($exception)
|
|
||||||
;
|
|
||||||
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
|
|
||||||
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', false);
|
|
||||||
|
|
||||||
$this->expectException(PontBasculeException::class);
|
|
||||||
$this->expectExceptionMessage('Erreur lors de la communication avec le pont bascule:');
|
|
||||||
|
|
||||||
$service->fetch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Tests\State;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\Get;
|
|
||||||
use App\Dto\PontBasculeReading;
|
|
||||||
use App\Service\PontBasculePayloadDecoder;
|
|
||||||
use App\Service\PontBasculeService;
|
|
||||||
use App\State\ReceptionWeighingProvider;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class ReceptionWeighingProviderTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testProvideReturnsReading(): void
|
|
||||||
{
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
|
|
||||||
$httpClient = $this->createMock(HttpClientInterface::class);
|
|
||||||
$httpClient->expects(self::never())->method('request');
|
|
||||||
|
|
||||||
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', true);
|
|
||||||
|
|
||||||
$provider = new ReceptionWeighingProvider($service);
|
|
||||||
|
|
||||||
$result = $provider->provide(new Get());
|
|
||||||
|
|
||||||
self::assertInstanceOf(PontBasculeReading::class, $result);
|
|
||||||
self::assertSame(121, $result->getDsd());
|
|
||||||
self::assertSame(1420.0, $result->getWeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testProvideThrowsHttpException(): void
|
|
||||||
{
|
|
||||||
$exception = $this->createStub(TransportExceptionInterface::class);
|
|
||||||
|
|
||||||
$httpClient = $this->createMock(HttpClientInterface::class);
|
|
||||||
$httpClient
|
|
||||||
->expects(self::once())
|
|
||||||
->method('request')
|
|
||||||
->willThrowException($exception)
|
|
||||||
;
|
|
||||||
|
|
||||||
$decoder = new PontBasculePayloadDecoder();
|
|
||||||
|
|
||||||
$service = new PontBasculeService($httpClient, $decoder, 'http://example.test', false);
|
|
||||||
|
|
||||||
$provider = new ReceptionWeighingProvider($service);
|
|
||||||
|
|
||||||
$this->expectException(HttpException::class);
|
|
||||||
$this->expectExceptionMessage('Erreur lors de la communication avec le pont bascule:');
|
|
||||||
|
|
||||||
$provider->provide(new Get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user