Ajout de la génération du bon de reception et correction des retours (!4)
Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Ferme/pulls/4
This commit is contained in:
Generated
+6
@@ -139,6 +139,12 @@
|
|||||||
<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" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
Generated
+6
@@ -145,6 +145,12 @@
|
|||||||
<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" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
<component name="PhpProjectSharedConfiguration" php_language_level="8.4" />
|
||||||
|
|||||||
Generated
+87
-25
@@ -4,10 +4,13 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : update du fichier README.md et CHANGELOG.md">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/composables/useApi.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/useApi.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package-lock.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
<execution />
|
<execution />
|
||||||
</component>
|
</component>
|
||||||
<component name="EmbeddingIndexingInfo">
|
<component name="EmbeddingIndexingInfo">
|
||||||
<option name="cachedIndexableFilesCount" value="137" />
|
<option name="cachedIndexableFilesCount" value="151" />
|
||||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="FileTemplateManagerImpl">
|
<component name="FileTemplateManagerImpl">
|
||||||
@@ -33,7 +36,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/connexion-pont-bascule" />
|
<entry key="$PROJECT_DIR$" value="develop" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
@@ -186,6 +189,12 @@
|
|||||||
<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" />
|
||||||
</include_path>
|
</include_path>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectColorInfo">{
|
<component name="ProjectColorInfo">{
|
||||||
@@ -205,7 +214,7 @@
|
|||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "feat/203-reception-parcours-pesee-multi-etapas",
|
"git-widget-placeholder": "feat/reception-generation-bon",
|
||||||
"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)",
|
||||||
@@ -222,6 +231,9 @@
|
|||||||
}</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>
|
||||||
@@ -242,7 +254,10 @@
|
|||||||
<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="23185000" />
|
<workItem from="1768287908317" duration="28058000" />
|
||||||
|
<workItem from="1768374298711" duration="12403000" />
|
||||||
|
<workItem from="1768460547451" duration="26946000" />
|
||||||
|
<workItem from="1768547023783" duration="11371000" />
|
||||||
</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" />
|
||||||
@@ -284,18 +299,77 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1768317786187</updated>
|
<updated>1768317786187</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="6" />
|
<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>
|
||||||
|
<option name="localTasksCounter" value="10" />
|
||||||
<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="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="develop" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</State>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
</map>
|
</map>
|
||||||
@@ -309,22 +383,10 @@
|
|||||||
<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" />
|
<MESSAGE value="feat : update du fichier README.md et CHANGELOG.md" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="feat : update du fichier README.md et CHANGELOG.md" />
|
<MESSAGE value="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple" />
|
||||||
</component>
|
<MESSAGE value="test : ajout de TU sur les services et providers" />
|
||||||
<component name="XDebuggerManager">
|
<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" />
|
||||||
<breakpoint-manager>
|
<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" />
|
||||||
<breakpoints>
|
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur" />
|
||||||
<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,17 +22,24 @@ 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.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"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",
|
||||||
"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",
|
||||||
|
|||||||
Generated
+436
-1
@@ -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": "3e883e3a506afa201779d16a950f4845",
|
"content-hash": "5cd56256b984963ecd4eaa17f2612f57",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/doctrine-common",
|
"name": "api-platform/doctrine-common",
|
||||||
@@ -2361,6 +2361,228 @@
|
|||||||
},
|
},
|
||||||
"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": "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",
|
||||||
@@ -2954,6 +3176,80 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -7135,6 +7431,145 @@
|
|||||||
],
|
],
|
||||||
"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",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.iziToast {
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iziToast > .iziToast-body {
|
||||||
|
padding: 18px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iziToast > .iziToast-body .iziToast-title {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iziToast > .iziToast-body .iziToast-message {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
@@ -2,15 +2,11 @@
|
|||||||
<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 class="flex flex-col">
|
<div>
|
||||||
<label for="license-plate" class="font-bold uppercase text-xl mb-4">Immatriculation</label>
|
<UiLicensePlateInput
|
||||||
<input
|
|
||||||
id="license-plate"
|
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
type="text"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
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>
|
||||||
@@ -20,24 +16,19 @@
|
|||||||
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"
|
||||||
>Valider
|
>Peser
|
||||||
</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 = {
|
||||||
@@ -45,62 +36,44 @@ 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: ''
|
receptionDate: new Date().toISOString().slice(0, 10)
|
||||||
})
|
|
||||||
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(
|
||||||
storeReception,
|
() => receptionStore.current,
|
||||||
(reception) => {
|
(reception) => {
|
||||||
form.licensePlate = reception?.licensePlate ?? ''
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
form.receptionDate = reception?.receptionDate ?? ''
|
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
|
||||||
},
|
},
|
||||||
{ 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({
|
|
||||||
licensePlate: normalizedLicensePlate,
|
if (!receptionStore.current) {
|
||||||
receptionDate: normalizedReceptionDate
|
const created = await receptionStore.createReception({
|
||||||
})
|
currentStep: 1,
|
||||||
if (!result.success) {
|
licensePlate: normalizedLicensePlate,
|
||||||
const errors = mapZodErrors<ReceptionFormData>(result.error)
|
receptionDate: normalizedReceptionDate
|
||||||
fieldErrors.licensePlate = errors.licensePlate ?? 'Formulaire invalide.'
|
})
|
||||||
fieldErrors.receptionDate = errors.receptionDate ?? 'Formulaire invalide.'
|
if (created) {
|
||||||
|
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 || null,
|
licensePlate: normalizedLicensePlate,
|
||||||
receptionDate: normalizedReceptionDate || null
|
receptionDate: normalizedReceptionDate
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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-3xl uppercase font-bold">Décharger les bêtes</h1>
|
<h1 class="text-4xl 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"
|
||||||
>Suivant</button>
|
>Peser</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -5,20 +5,19 @@
|
|||||||
<!--@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="errorMessage || showLoadingBox"
|
v-if="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]">
|
||||||
<p v-if="errorMessage" class="text-red-500">{{ errorMessage }}</p>
|
<UiLoadingDots />
|
||||||
<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>
|
||||||
@@ -28,15 +27,22 @@
|
|||||||
@click="fetchWeight"
|
@click="fetchWeight"
|
||||||
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
||||||
<button
|
<button
|
||||||
v-if="displayWeight !== null"
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="saveWeight"
|
@click="saveWeight"
|
||||||
>Valider la pesée</button>
|
>Valider la pesée</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'
|
||||||
@@ -45,13 +51,18 @@ const props = defineProps<{
|
|||||||
mode: 'gross' | 'tare'
|
mode: 'gross' | 'tare'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const { current: storeReception, errorMessage: storeErrorMessage } = storeToRefs(receptionStore)
|
const { current: storeReception } = storeToRefs(receptionStore)
|
||||||
|
type PdfPrinterHandle = {
|
||||||
|
print: (url: string) => Promise<void>
|
||||||
|
}
|
||||||
|
// Ref sur le composant d'impression pour déclencher le print() du PDF.
|
||||||
|
const pdfPrinter = ref<PdfPrinterHandle | null>(null)
|
||||||
const {
|
const {
|
||||||
displayWeight,
|
displayWeight,
|
||||||
displayDsd,
|
displayDsd,
|
||||||
title,
|
title,
|
||||||
errorMessage,
|
|
||||||
showLoadingBox,
|
showLoadingBox,
|
||||||
fetchWeight,
|
fetchWeight,
|
||||||
saveWeight
|
saveWeight
|
||||||
@@ -59,9 +70,35 @@ 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
|
|
||||||
})
|
})
|
||||||
// @TODO Voir comment mettre en place la genération du bon, la validation de la reception et le dernier step
|
const showGenerateReceipt = computed(
|
||||||
|
() => props.mode === 'tare' && displayWeight.value !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
const printReceipt = async () => {
|
||||||
|
if (!import.meta.client || !receptionStore.current || !pdfPrinter.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWeight()
|
||||||
|
await pdfPrinter.value.print(`/receptions/${receptionStore.current.id}/receipt`)
|
||||||
|
|
||||||
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
const result = await receptionStore.updateReception(receptionStore.current.id, {
|
||||||
|
isValid: true
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
receptionStore.clearCurrent()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchWeight()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<iframe ref="printFrame" class="hidden" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
|
|
||||||
|
const printFrame = ref<HTMLIFrameElement | null>(null)
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
// Expose une methode simple pour imprimer un PDF depuis les ecrans.
|
||||||
|
const print = async (url: string): Promise<void> => {
|
||||||
|
return printPdf(url, printFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
print
|
||||||
|
})
|
||||||
|
</script>
|
||||||
+116
-12
@@ -4,22 +4,123 @@ import { $fetch, FetchError } from 'ofetch'
|
|||||||
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?: FetchOptions<'json'>): Promise<T>
|
get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
post<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<Blob>
|
||||||
put<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
patch<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
delete<T>(url: string, query?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
|
patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
|
delete<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
|
||||||
|
FetchOptions<ResponseType> & {
|
||||||
|
toast?: boolean
|
||||||
|
toastTitle?: string
|
||||||
|
toastErrorMessage?: string
|
||||||
|
toastSuccessMessage?: string
|
||||||
|
toastErrorKey?: string
|
||||||
|
toastSuccessKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const useApi = (): ApiClient => {
|
export const useApi = (): ApiClient => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const baseURL = config.public.apiBase ?? '/api'
|
const baseURL = config.public.apiBase ?? '/api'
|
||||||
const client = $fetch.create({ baseURL, retry: 0 })
|
const toast = useToast()
|
||||||
|
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,
|
||||||
|
onResponse({ options }) {
|
||||||
|
const apiOptions = options as ApiFetchOptions<'json'>
|
||||||
|
if (apiOptions?.toast === false) {
|
||||||
|
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: FetchOptions<'json'> = {}
|
options: ApiFetchOptions<'json'> = {}
|
||||||
) => {
|
) => {
|
||||||
const needsJsonBody = method === 'POST' || method === 'PUT'
|
const needsJsonBody = method === 'POST' || method === 'PUT'
|
||||||
const needsMergePatch = method === 'PATCH'
|
const needsMergePatch = method === 'PATCH'
|
||||||
@@ -36,19 +137,22 @@ export const useApi = (): ApiClient => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get<T>(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
return request<T>('GET', url, { ...options, query })
|
return request<T>('GET', url, { ...options, query })
|
||||||
},
|
},
|
||||||
post<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
|
||||||
|
return client<Blob>(url, { ...options, method: 'GET', query, responseType: 'blob' })
|
||||||
|
},
|
||||||
|
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
return request<T>('POST', url, { ...options, body })
|
return request<T>('POST', url, { ...options, body })
|
||||||
},
|
},
|
||||||
put<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
return request<T>('PUT', url, { ...options, body })
|
return request<T>('PUT', url, { ...options, body })
|
||||||
},
|
},
|
||||||
patch<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
return request<T>('PATCH', url, { ...options, body })
|
return request<T>('PATCH', url, { ...options, body })
|
||||||
},
|
},
|
||||||
delete<T>(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) {
|
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
|
||||||
return request<T>('DELETE', url, { ...options, query })
|
return request<T>('DELETE', url, { ...options, query })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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 { computed, ref } from 'vue'
|
import type {Ref} from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import {computed, 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,18 +12,16 @@ 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 localErrorMessage = ref<string | null>(null)
|
const isFetching = ref(false)
|
||||||
|
|
||||||
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
||||||
const weights = reception.value?.weights ?? []
|
const weights = reception.value?.weights ?? []
|
||||||
@@ -33,22 +31,19 @@ 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 errorMessage = computed(() => localErrorMessage.value ?? storeError?.value ?? null)
|
const showLoadingBox = computed(
|
||||||
const showLoadingBox = computed(() => displayWeight.value === null && !errorMessage.value)
|
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
||||||
|
)
|
||||||
|
|
||||||
const fetchWeight = async () => {
|
const fetchWeight = async () => {
|
||||||
localErrorMessage.value = null
|
isFetching.value = true
|
||||||
try {
|
weightData.value = await getWeight().finally(() => {
|
||||||
weightData.value = await getWeight()
|
isFetching.value = false
|
||||||
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,36 +53,32 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (existingEntry?.id) {
|
||||||
if (existingEntry?.id) {
|
await updateWeight(existingEntry.id, {
|
||||||
await updateWeight(existingEntry.id, {
|
type: mode,
|
||||||
type: mode,
|
dsd: baseDsd,
|
||||||
dsd: baseDsd,
|
weight: baseWeight,
|
||||||
weight: baseWeight,
|
weighedAt: baseWeighedAt
|
||||||
weighedAt: baseWeighedAt
|
})
|
||||||
})
|
} else {
|
||||||
} else {
|
await createWeight({
|
||||||
await createWeight({
|
reception: `/receptions/${reception.value.id}`,
|
||||||
reception: `/receptions/${reception.value.id}`,
|
type: mode,
|
||||||
type: mode,
|
dsd: baseDsd,
|
||||||
dsd: baseDsd,
|
weight: baseWeight,
|
||||||
weight: baseWeight,
|
weighedAt: baseWeighedAt
|
||||||
weighedAt: baseWeighedAt
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = reception.value.currentStep + 1
|
const nextStep = mode === 'tare'
|
||||||
|
? reception.value.currentStep
|
||||||
|
: reception.value.currentStep + 1
|
||||||
await updateReception(reception.value.id, {
|
await updateReception(reception.value.id, {
|
||||||
currentStep: nextStep,
|
currentStep: nextStep,
|
||||||
isValid: mode === 'tare' ? true : reception.value.isValid
|
isValid: reception.value.isValid
|
||||||
})
|
})
|
||||||
|
|
||||||
if (loadReception) {
|
if (loadReception) {
|
||||||
@@ -101,7 +92,6 @@ export const useWeighing = ({
|
|||||||
displayWeight,
|
displayWeight,
|
||||||
displayDsd,
|
displayDsd,
|
||||||
title,
|
title,
|
||||||
errorMessage,
|
|
||||||
showLoadingBox,
|
showLoadingBox,
|
||||||
fetchWeight,
|
fetchWeight,
|
||||||
saveWeight
|
saveWeight
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"reception": {
|
||||||
|
"update": "Réception mise à jour avec succès."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-2
@@ -2,13 +2,34 @@ export default defineNuxtConfig({
|
|||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
ssr: false,
|
ssr: false,
|
||||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Generated
+2457
-5
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,12 @@
|
|||||||
"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,14 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="errorMessage" class="text-red-600">{{ errorMessage }}</div>
|
<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>
|
||||||
<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>
|
<button
|
||||||
|
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?.currentStep === 0"/>
|
<ReceptionForm v-if="!storeReception || 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 === 3" mode="tare"/>
|
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,17 +23,41 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const { current: storeReception, errorMessage } = storeToRefs(receptionStore)
|
const { current: storeReception } = storeToRefs(receptionStore)
|
||||||
|
|
||||||
onMounted(async () => {
|
const resolveReceptionId = (param: unknown) => {
|
||||||
const raw = route.params.id
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
const idStr = Array.isArray(raw) ? raw[0] : raw
|
if (!idStr) {
|
||||||
const id = idStr ? Number(idStr) : null
|
return null
|
||||||
|
|
||||||
if (id === null) {
|
|
||||||
await receptionStore.createReception()
|
|
||||||
} else {
|
|
||||||
await receptionStore.loadReception(id)
|
|
||||||
}
|
}
|
||||||
})
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
async (param) => {
|
||||||
|
const id = resolveReceptionId(param)
|
||||||
|
if (id === null) {
|
||||||
|
receptionStore.clearCurrent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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,50 +1,39 @@
|
|||||||
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() {
|
||||||
try {
|
const api = useApi()
|
||||||
return await api.get<ReceptionData>(`receptions`)
|
return api.get<ReceptionData>(`receptions`, {}, {
|
||||||
} catch (error) {
|
toastErrorKey: 'errors.reception.list'
|
||||||
console.error(error.message, error)
|
})
|
||||||
return error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReception(id: number) {
|
export async function getReception(id: number) {
|
||||||
try {
|
const api = useApi()
|
||||||
return await api.get<ReceptionData>(`receptions/${id}`)
|
return api.get<ReceptionData>(`receptions/${id}`, {}, {
|
||||||
} catch (error) {
|
toastErrorKey: 'errors.reception.fetch'
|
||||||
console.error(error.message, error)
|
})
|
||||||
return error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createReception(payload: Partial<ReceptionData> = {}) {
|
export async function createReception(payload: Partial<ReceptionData> = {}) {
|
||||||
try {
|
const api = useApi()
|
||||||
return await api.post<ReceptionData>('receptions', payload)
|
return api.post<ReceptionData>('receptions', payload, {
|
||||||
} catch (error) {
|
toastErrorKey: 'errors.reception.create'
|
||||||
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>) {
|
||||||
try {
|
const api = useApi()
|
||||||
return await api.patch<ReceptionData>(`receptions/${id}`, payload)
|
return api.patch<ReceptionData>(`receptions/${id}`, payload, {
|
||||||
} catch (error) {
|
toastErrorKey: 'errors.reception.update',
|
||||||
console.error(error.message, error)
|
toastSuccessKey: 'success.reception.update'
|
||||||
return error
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWeight(): Promise<WeightData> {
|
export async function getWeight(): Promise<WeightData> {
|
||||||
try {
|
const api = useApi()
|
||||||
return await api.get<WeightData>('receptions/weigh')
|
return api.get<WeightData>('receptions/weigh', {}, {
|
||||||
} catch (error) {
|
toastErrorKey: 'errors.reception.weigh'
|
||||||
console.error(error.message, error)
|
})
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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'
|
||||||
@@ -12,19 +10,11 @@ export type WeightPayload = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createWeight(payload: WeightPayload) {
|
export async function createWeight(payload: WeightPayload) {
|
||||||
try {
|
const api = useApi()
|
||||||
return await api.post<WeightEntryData>('weights', payload)
|
return 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>) {
|
||||||
try {
|
const api = useApi()
|
||||||
return await api.patch<WeightEntryData>(`weights/${id}`, payload)
|
return api.patch<WeightEntryData>(`weights/${id}`, payload)
|
||||||
} catch (error) {
|
|
||||||
console.error(error.message, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,64 +9,51 @@ 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
|
||||||
},
|
},
|
||||||
clearError() {
|
clearCurrent() {
|
||||||
this.errorMessage = null
|
this.current = null
|
||||||
},
|
},
|
||||||
async loadReception(id: number) {
|
async loadReception(id: number) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errorMessage = null
|
const result = await getReception(id).finally(() => {
|
||||||
try {
|
|
||||||
const result = await getReception(id)
|
|
||||||
if (!isReceptionData(result)) {
|
|
||||||
this.errorMessage = 'Réception introuvable.'
|
|
||||||
this.current = null
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = result
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isReceptionData(result)) {
|
||||||
|
this.current = null
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
async createReception() {
|
async createReception(payload: Partial<ReceptionData> = {}) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errorMessage = null
|
const result = await createReception(payload).finally(() => {
|
||||||
try {
|
|
||||||
const result = await createReception()
|
|
||||||
if (!isReceptionData(result)) {
|
|
||||||
this.errorMessage = 'Impossible de créer la réception.'
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = result
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isReceptionData(result)) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
async updateReception(id: number, payload: Partial<ReceptionData>) {
|
async updateReception(id: number, payload: Partial<ReceptionData>) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.errorMessage = null
|
const result = await updateReception(id, payload).finally(() => {
|
||||||
try {
|
|
||||||
const result = await updateReception(id, payload)
|
|
||||||
if (!isReceptionData(result)) {
|
|
||||||
this.errorMessage = 'Impossible de mettre à jour la réception.'
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = result
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isReceptionData(result)) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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;
|
||||||
@@ -51,6 +52,16 @@ 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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)]
|
)]
|
||||||
class Reception
|
class Reception
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?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.'"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<!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>
|
||||||
Reference in New Issue
Block a user