Compare commits
78 Commits
feat/admin
...
v0.0.70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
995e7de2cc | ||
| 2408ccab67 | |||
| 82af4d4c1e | |||
|
|
11491b02c5 | ||
| 024af5887e | |||
|
|
91c0125876 | ||
| b510cdcc42 | |||
|
|
d0213c3212 | ||
| 3ac676689d | |||
|
|
9f47e81efd | ||
| 257b93e691 | |||
|
|
dc5320b324 | ||
| 09a641e5cf | |||
|
|
a0557b077b | ||
| 2d2b38eae4 | |||
|
|
d3581b8ce6 | ||
| 9e53be8ac3 | |||
|
|
2aafa2082a | ||
| 2b64f024b6 | |||
|
|
47cac04257 | ||
| 59d76c5f14 | |||
|
|
c48cc477da | ||
| 5967665e9f | |||
|
|
393c420983 | ||
| 456623b403 | |||
|
|
e2a8e89e55 | ||
| 92a5c48e5e | |||
|
|
6766985713 | ||
| c0d05264df | |||
|
|
9505201499 | ||
| 624591c096 | |||
|
|
e31bdce713 | ||
| 5d72beaf8d | |||
| 43f34015c6 | |||
|
|
ac5ce07e61 | ||
| e9fb36cc24 | |||
|
|
06a41c5f85 | ||
| f263a11fe8 | |||
|
|
c52f22472d | ||
| e7421e985e | |||
|
|
0d258ae9c6 | ||
| 7dd615ea34 | |||
|
|
6eee0745a7 | ||
| 845f94db8c | |||
|
|
86c0e74074 | ||
| be29daf4d1 | |||
|
|
08e7c1508c | ||
| 358da6a8ad | |||
|
|
67428186f6 | ||
| 09d108a1d5 | |||
|
|
f58dc36a0d | ||
| 15c0f414af | |||
|
|
9ed0ba702e | ||
| 93edd0a563 | |||
|
|
c361ef9bb9 | ||
| 7f3d9ef9c6 | |||
|
|
22b959de85 | ||
| d3bc2e11f1 | |||
|
|
d8b16f5e15 | ||
| 43213bc6d6 | |||
|
|
09666d9319 | ||
| 05ea33735d | |||
|
|
89c67f7e97 | ||
| d527e94bac | |||
|
|
579bdba65b | ||
| b1c3952d09 | |||
|
|
ab6de16319 | ||
| 800ab1d432 | |||
|
|
fade51d3ee | ||
| 9ca0a7511b | |||
|
|
d3dfde7060 | ||
| 90c2cfc665 | |||
|
|
9fc3e2f9bc | ||
| 329bb4cee5 | |||
|
|
d3af654858 | ||
| 168d8c78eb | |||
|
|
338d903cef | ||
| 42ce1e2d08 |
19
.idea/dataSources.xml
generated
19
.idea/dataSources.xml
generated
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
|
||||||
<data-source source="LOCAL" name="ferme" uuid="f407a514-c6b4-4b26-9555-445a85892502">
|
|
||||||
<driver-ref>postgresql</driver-ref>
|
|
||||||
<synchronize>true</synchronize>
|
|
||||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
|
||||||
<jdbc-url>jdbc:postgresql://localhost:5432/ferme</jdbc-url>
|
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
|
||||||
</data-source>
|
|
||||||
<data-source source="LOCAL" name="Ferme recette" uuid="ae622167-c834-4e7b-87a5-c1721036f5dc">
|
|
||||||
<driver-ref>postgresql</driver-ref>
|
|
||||||
<synchronize>true</synchronize>
|
|
||||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
|
||||||
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
|
|
||||||
<working-dir>$ProjectFileDir$</working-dir>
|
|
||||||
</data-source>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
2
.idea/db-forest-config.xml
generated
2
.idea/db-forest-config.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="db-tree-configuration">
|
<component name="db-tree-configuration">
|
||||||
<option name="data" value="---------------------------------------- 1:0:f407a514-c6b4-4b26-9555-445a85892502 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc " />
|
<option name="data" value="---------------------------------------- 1:0:f407a514-c6b4-4b26-9555-445a85892502 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc 3:0:9cad43df-2147-4989-b7a4-443067034884 4:0:09e221b8-067a-488b-9c1d-4e155a333079 " />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="PhpCSFixerValidationInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
1
.idea/php.xml
generated
1
.idea/php.xml
generated
@@ -15,6 +15,7 @@
|
|||||||
<component name="PhpCSFixer">
|
<component name="PhpCSFixer">
|
||||||
<phpcsfixer_settings>
|
<phpcsfixer_settings>
|
||||||
<PhpCSFixerConfiguration tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
|
<PhpCSFixerConfiguration tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
|
||||||
|
<phpcs_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="990ff521-e6e9-4080-9cc9-228367d597f9" tool_path="\\wsl.localhost\Ubuntu-24.04\home\matte\Ferme\vendor\bin\php-cs-fixer" timeout="30000" />
|
||||||
</phpcsfixer_settings>
|
</phpcsfixer_settings>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpCodeSniffer">
|
<component name="PhpCodeSniffer">
|
||||||
|
|||||||
387
.idea/workspace.xml
generated
387
.idea/workspace.xml
generated
@@ -4,16 +4,17 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : test auto-tag-develop.yml (auto incrément version)">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu">
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/composables/useAppVersion.ts" afterDir="false" />
|
|
||||||
<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/packages/security.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/packages/security.yaml" 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/app.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/app.vue" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/components/shipment/shipment-loading.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/shipment/shipment-loading.vue" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/components/shipment/shipment-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/shipment/shipment-weight.vue" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/composables/useWeighing.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/useWeighing.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/layouts/default.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/layouts/default.vue" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/layouts/default.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/layouts/default.vue" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/pages/login.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/login.vue" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/services/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/Dto/AppVersion.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/ApiResource/AppVersion.php" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/frontend/services/shipment.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/shipment.ts" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/State/AppVersionProvider.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/State/AppVersionProvider.php" 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" />
|
||||||
@@ -36,19 +37,23 @@
|
|||||||
<component name="FileTemplateManagerImpl">
|
<component name="FileTemplateManagerImpl">
|
||||||
<option name="RECENT_TEMPLATES">
|
<option name="RECENT_TEMPLATES">
|
||||||
<list>
|
<list>
|
||||||
<option value="Vue Composition API Component" />
|
|
||||||
<option value="TypeScript File" />
|
<option value="TypeScript File" />
|
||||||
|
<option value="PHP File" />
|
||||||
|
<option value="Vue Composition API Component" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<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/256-reception-etape-3-bovin" />
|
<entry key="$PROJECT_DIR$" value="feat/327-voir-modifier-une-expedition-terminee" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
</component>
|
||||||
<component name="McpProjectServerCommands">
|
<component name="McpProjectServerCommands">
|
||||||
<commands />
|
<commands />
|
||||||
<urls />
|
<urls />
|
||||||
@@ -233,13 +238,13 @@
|
|||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "develop",
|
"git-widget-placeholder": "develop",
|
||||||
"last_opened_file_path": "/home/sroy/Documents/test/Ferme",
|
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/m-tristan/workspace/Ferme",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "configurable.tailwindcss",
|
"settings.editor.selected.configurable": "advanced.settings",
|
||||||
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
},
|
},
|
||||||
@@ -256,6 +261,13 @@
|
|||||||
}
|
}
|
||||||
}</component>
|
}</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||||
|
<recent name="$PROJECT_DIR$/frontend/components/commun" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\pages\shipment" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\composables" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\components\shipment" />
|
||||||
|
</key>
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||||
@@ -267,10 +279,11 @@
|
|||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
<set>
|
<set>
|
||||||
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.30387.85" />
|
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.31033.138" />
|
||||||
</set>
|
</set>
|
||||||
</attachedChunks>
|
</attachedChunks>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
<component name="TaskManager">
|
<component name="TaskManager">
|
||||||
<task active="true" id="Default" summary="Default task">
|
<task active="true" id="Default" summary="Default task">
|
||||||
<changelist id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="" />
|
<changelist id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="" />
|
||||||
@@ -301,118 +314,22 @@
|
|||||||
<workItem from="1770195718952" duration="215000" />
|
<workItem from="1770195718952" duration="215000" />
|
||||||
<workItem from="1770195959162" duration="18915000" />
|
<workItem from="1770195959162" duration="18915000" />
|
||||||
<workItem from="1770274844804" duration="3940000" />
|
<workItem from="1770274844804" duration="3940000" />
|
||||||
</task>
|
<workItem from="1770798536017" duration="20774000" />
|
||||||
<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">
|
<workItem from="1770879701502" duration="25805000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1770966186589" duration="914000" />
|
||||||
<created>1768555180530</created>
|
<workItem from="1770967274060" duration="2388000" />
|
||||||
<option name="number" value="00009" />
|
<workItem from="1772466451823" duration="598000" />
|
||||||
<option name="presentableId" value="LOCAL-00009" />
|
<workItem from="1772626984813" duration="969000" />
|
||||||
<option name="project" value="LOCAL" />
|
<workItem from="1772786360430" duration="21000" />
|
||||||
<updated>1768555180530</updated>
|
<workItem from="1772786475316" duration="3016000" />
|
||||||
</task>
|
<workItem from="1773049125640" duration="406000" />
|
||||||
<task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik">
|
<workItem from="1773049540928" duration="539000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1773050154207" duration="1879000" />
|
||||||
<created>1768832208350</created>
|
<workItem from="1773212999001" duration="652000" />
|
||||||
<option name="number" value="00010" />
|
<workItem from="1773215356754" duration="5754000" />
|
||||||
<option name="presentableId" value="LOCAL-00010" />
|
<workItem from="1773756072697" duration="5450000" />
|
||||||
<option name="project" value="LOCAL" />
|
<workItem from="1773766075191" duration="6202000" />
|
||||||
<updated>1768832208350</updated>
|
<workItem from="1773824491213" duration="1794000" />
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00011" summary="feat : update du CHANGELOG.md">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1768832516587</created>
|
|
||||||
<option name="number" value="00011" />
|
|
||||||
<option name="presentableId" value="LOCAL-00011" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768832516587</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00012" summary="fix : correction de l'accès au swagger en mode dev qui n'était plus accessible">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1768940104944</created>
|
|
||||||
<option name="number" value="00012" />
|
|
||||||
<option name="presentableId" value="LOCAL-00012" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768940104944</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00013" summary="feat : ajout de la conf pour le déploiement en recette">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769005220331</created>
|
|
||||||
<option name="number" value="00013" />
|
|
||||||
<option name="presentableId" value="LOCAL-00013" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769005220331</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00014" summary="fix : fix de la conf pour le déploiement en recette">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769008700008</created>
|
|
||||||
<option name="number" value="00014" />
|
|
||||||
<option name="presentableId" value="LOCAL-00014" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769008700008</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00015" summary="fix : fix de la conf pour le déploiement en recette">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769014602062</created>
|
|
||||||
<option name="number" value="00015" />
|
|
||||||
<option name="presentableId" value="LOCAL-00015" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769014602062</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00016" summary="fix : migration apache vers nginx pour un déploiement plus simple">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769019284586</created>
|
|
||||||
<option name="number" value="00016" />
|
|
||||||
<option name="presentableId" value="LOCAL-00016" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769019284586</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00017" summary="fix : dernière modification pour le déploiement en recette et le changement de conf vers nginx">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769021756823</created>
|
|
||||||
<option name="number" value="00017" />
|
|
||||||
<option name="presentableId" value="LOCAL-00017" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769021756823</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00018" summary="ci : auto tag + release artefact">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769021818384</created>
|
|
||||||
<option name="number" value="00018" />
|
|
||||||
<option name="presentableId" value="LOCAL-00018" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769021818384</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00019" summary="ci : fix release artefact">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769022071620</created>
|
|
||||||
<option name="number" value="00019" />
|
|
||||||
<option name="presentableId" value="LOCAL-00019" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769022071620</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00020" summary="ci : fix release artefact">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769024603812</created>
|
|
||||||
<option name="number" value="00020" />
|
|
||||||
<option name="presentableId" value="LOCAL-00020" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769024603812</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00021" summary="ci : ajout du script et de la doc déploiement">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769026716634</created>
|
|
||||||
<option name="number" value="00021" />
|
|
||||||
<option name="presentableId" value="LOCAL-00021" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769026716634</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00022" summary="fix : correction du path URI pour la création d'un poids dans une réception">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769073690382</created>
|
|
||||||
<option name="number" value="00022" />
|
|
||||||
<option name="presentableId" value="LOCAL-00022" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769073690382</updated>
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00023" summary="feat : Ajout du bundle Monolog pour la gestion des logs">
|
<task id="LOCAL-00023" summary="feat : Ajout du bundle Monolog pour la gestion des logs">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -630,71 +547,183 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770217875423</updated>
|
<updated>1770217875423</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00050" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
<task id="LOCAL-00050" summary="feat : creer une nouvelle expedtion (WIP)">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770283622425</created>
|
<created>1770736570645</created>
|
||||||
<option name="number" value="00050" />
|
<option name="number" value="00050" />
|
||||||
<option name="presentableId" value="LOCAL-00050" />
|
<option name="presentableId" value="LOCAL-00050" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770283622425</updated>
|
<updated>1770736570645</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00051" summary="feat : ajout du responsive sur la navbar et la page d'accueil">
|
<task id="LOCAL-00051" summary="feat : ajout d'une page de creation d'une expedition">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770308927948</created>
|
<created>1770880791564</created>
|
||||||
<option name="number" value="00051" />
|
<option name="number" value="00051" />
|
||||||
<option name="presentableId" value="LOCAL-00051" />
|
<option name="presentableId" value="LOCAL-00051" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770308927948</updated>
|
<updated>1770880791565</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00052" summary="fix : logo centré en mod mobile">
|
<task id="LOCAL-00052" summary="feat : changelog">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770310504254</created>
|
<created>1770881437439</created>
|
||||||
<option name="number" value="00052" />
|
<option name="number" value="00052" />
|
||||||
<option name="presentableId" value="LOCAL-00052" />
|
<option name="presentableId" value="LOCAL-00052" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770310504254</updated>
|
<updated>1770881437439</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00053" summary="feat : ajout d'un numéro de version automatique via la CI">
|
<task id="LOCAL-00053" summary="feat : lister les expeditions terminees">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770369945257</created>
|
<created>1770883114609</created>
|
||||||
<option name="number" value="00053" />
|
<option name="number" value="00053" />
|
||||||
<option name="presentableId" value="LOCAL-00053" />
|
<option name="presentableId" value="LOCAL-00053" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770369945257</updated>
|
<updated>1770883114609</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00054" summary="feat : update numéro de version">
|
<task id="LOCAL-00054" summary="feat : lister les expeditions terminees">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770370216428</created>
|
<created>1770884154297</created>
|
||||||
<option name="number" value="00054" />
|
<option name="number" value="00054" />
|
||||||
<option name="presentableId" value="LOCAL-00054" />
|
<option name="presentableId" value="LOCAL-00054" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770370216428</updated>
|
<updated>1770884154297</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00055" summary="fix : auto-tag-develop.yml">
|
<task id="LOCAL-00055" summary="fix : corrections diverses">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770370700697</created>
|
<created>1770969471135</created>
|
||||||
<option name="number" value="00055" />
|
<option name="number" value="00055" />
|
||||||
<option name="presentableId" value="LOCAL-00055" />
|
<option name="presentableId" value="LOCAL-00055" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770370700698</updated>
|
<updated>1770969471135</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00056" summary="fix : auto-tag-develop.yml">
|
<task id="LOCAL-00056" summary="fix : corrections frontend">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770370919043</created>
|
<created>1772094268366</created>
|
||||||
<option name="number" value="00056" />
|
<option name="number" value="00056" />
|
||||||
<option name="presentableId" value="LOCAL-00056" />
|
<option name="presentableId" value="LOCAL-00056" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770370919043</updated>
|
<updated>1772094268366</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00057" summary="feat : test auto-tag-develop.yml (auto incrément version)">
|
<task id="LOCAL-00057" summary="feat : affichage et modification expédition et modification bouton valider">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770371073055</created>
|
<created>1772111964268</created>
|
||||||
<option name="number" value="00057" />
|
<option name="number" value="00057" />
|
||||||
<option name="presentableId" value="LOCAL-00057" />
|
<option name="presentableId" value="LOCAL-00057" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770371073055</updated>
|
<updated>1772111964268</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="58" />
|
<task id="LOCAL-00058" summary="fix : erreur customer adress et bouton valider oublie">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772112729501</created>
|
||||||
|
<option name="number" value="00058" />
|
||||||
|
<option name="presentableId" value="LOCAL-00058" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772112729502</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00059" summary="feat : changelog update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772112812677</created>
|
||||||
|
<option name="number" value="00059" />
|
||||||
|
<option name="presentableId" value="LOCAL-00059" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772112812677</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00060" summary="feat : changelog update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772177400063</created>
|
||||||
|
<option name="number" value="00060" />
|
||||||
|
<option name="presentableId" value="LOCAL-00060" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772177400063</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00061" summary="feat : changelog update">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772177614438</created>
|
||||||
|
<option name="number" value="00061" />
|
||||||
|
<option name="presentableId" value="LOCAL-00061" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772177614438</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00062" summary="fix : color tab">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772178540489</created>
|
||||||
|
<option name="number" value="00062" />
|
||||||
|
<option name="presentableId" value="LOCAL-00062" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772178540489</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00063" summary="feat : modification front de la page admin transporteur">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180053740</created>
|
||||||
|
<option name="number" value="00063" />
|
||||||
|
<option name="presentableId" value="LOCAL-00063" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180053740</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00064" summary="fix : espacement et changelog">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180581178</created>
|
||||||
|
<option name="number" value="00064" />
|
||||||
|
<option name="presentableId" value="LOCAL-00064" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180581178</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00065" summary="fix : espacement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180684250</created>
|
||||||
|
<option name="number" value="00065" />
|
||||||
|
<option name="presentableId" value="LOCAL-00065" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180684250</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00066" summary="fix : espacement">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772180972984</created>
|
||||||
|
<option name="number" value="00066" />
|
||||||
|
<option name="presentableId" value="LOCAL-00066" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772180972984</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00067" summary="fix : text">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772182545592</created>
|
||||||
|
<option name="number" value="00067" />
|
||||||
|
<option name="presentableId" value="LOCAL-00067" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772182545592</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00068" summary="feat : front page admin bovin et changelog">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772182707441</created>
|
||||||
|
<option name="number" value="00068" />
|
||||||
|
<option name="presentableId" value="LOCAL-00068" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772182707441</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00069" summary="fix : on ne bloque plus le poids max d'une pesée">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772447581744</created>
|
||||||
|
<option name="number" value="00069" />
|
||||||
|
<option name="presentableId" value="LOCAL-00069" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772447581744</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00070" summary="feat : ajout de supplier dans la feed et fixtures">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773761787472</created>
|
||||||
|
<option name="number" value="00070" />
|
||||||
|
<option name="presentableId" value="LOCAL-00070" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773761787472</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00071" summary="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773766207721</created>
|
||||||
|
<option name="number" value="00071" />
|
||||||
|
<option name="presentableId" value="LOCAL-00071" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773766207721</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="72" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -744,32 +773,52 @@
|
|||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="fix : gitea workflow" />
|
|
||||||
<MESSAGE value="fix : script de déploiement" />
|
|
||||||
<MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" />
|
|
||||||
<MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" />
|
|
||||||
<MESSAGE value="feat : ajout de la debug bar en mod dev" />
|
|
||||||
<MESSAGE value="feat : ajout du bundle Malio ednotif pour l'utilisation des WS" />
|
|
||||||
<MESSAGE value="fix : modification de la conf du bundle ednotif" />
|
|
||||||
<MESSAGE value="feat : update du CHANGELOG.md" />
|
|
||||||
<MESSAGE value="feat : finalisation de l'étape 1 "Réception" (formulaire)" />
|
|
||||||
<MESSAGE value="feat : ajout du numéro identification des receptions et ajustement du bon de reception" />
|
|
||||||
<MESSAGE value="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception" />
|
|
||||||
<MESSAGE value="feat : mise en place de composant UI pour les select, checkbox, date, text" />
|
|
||||||
<MESSAGE value="feat : update CHANGELOG.md" />
|
|
||||||
<MESSAGE value="feat : ajout de commentaire" />
|
<MESSAGE value="feat : ajout de commentaire" />
|
||||||
<MESSAGE value="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception" />
|
<MESSAGE value="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception" />
|
||||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception" />
|
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception" />
|
||||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||||
<MESSAGE value="feat : mise à jour du bon de réception" />
|
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||||
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||||
<MESSAGE value="feat : ajout du responsive sur la navbar et la page d'accueil" />
|
<MESSAGE value="feat : creer une nouvelle expedtion (WIP)" />
|
||||||
<MESSAGE value="fix : logo centré en mod mobile" />
|
<MESSAGE value="feat : ajout d'une page de creation d'une expedition" />
|
||||||
<MESSAGE value="feat : ajout d'un numéro de version automatique via la CI" />
|
<MESSAGE value="feat : changelog" />
|
||||||
<MESSAGE value="feat : update numéro de version" />
|
<MESSAGE value="feat : lister les expeditions terminees" />
|
||||||
<MESSAGE value="fix : auto-tag-develop.yml" />
|
<MESSAGE value="fix: corrections diverses" />
|
||||||
<MESSAGE value="feat : test auto-tag-develop.yml (auto incrément version)" />
|
<MESSAGE value="fix : corrections diverses" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="feat : test auto-tag-develop.yml (auto incrément version)" />
|
<MESSAGE value="fix : corrections frontend" />
|
||||||
|
<MESSAGE value="feat : affichage et modification expédition et modification bouton valider" />
|
||||||
|
<MESSAGE value="fix : erreur customer adress et bouton valider oublie" />
|
||||||
|
<MESSAGE value="feat : changelog update" />
|
||||||
|
<MESSAGE value="fix : color tab" />
|
||||||
|
<MESSAGE value="feat : modification front de la page admin transporteur" />
|
||||||
|
<MESSAGE value="fix : espacement et changelog" />
|
||||||
|
<MESSAGE value="fix : espacement" />
|
||||||
|
<MESSAGE value="fix : text" />
|
||||||
|
<MESSAGE value="feat : front page admin bovin et changelog" />
|
||||||
|
<MESSAGE value="fix : on ne bloque plus le poids max d'une pesée" />
|
||||||
|
<MESSAGE value="feat : ajout de supplier dans la feed et fixtures" />
|
||||||
|
<MESSAGE value="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu" />
|
||||||
|
</component>
|
||||||
|
<component name="XDebuggerManager">
|
||||||
|
<breakpoint-manager>
|
||||||
|
<breakpoints>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/Entity/ReceptionPelletBuilding.php</url>
|
||||||
|
<line>6</line>
|
||||||
|
<option name="timeStamp" value="3" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/Entity/Shipment.php</url>
|
||||||
|
<line>6</line>
|
||||||
|
<option name="timeStamp" value="45" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
|
<url>file://$PROJECT_DIR$/frontend/services/dto/shipment-data.ts</url>
|
||||||
|
<option name="timeStamp" value="43" />
|
||||||
|
</line-breakpoint>
|
||||||
|
</breakpoints>
|
||||||
|
</breakpoint-manager>
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
|
|||||||
59
AGENTS.md
59
AGENTS.md
@@ -1,59 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
Project overview
|
|
||||||
- Symfony 8 + API Platform 4 backend, Nuxt 3 frontend in `frontend/`.
|
|
||||||
- Apache vhost serves API under `/api` and frontend from `frontend/dist`.
|
|
||||||
- API base URL on frontend uses `NUXT_PUBLIC_API_BASE` (see `frontend/.env`).
|
|
||||||
|
|
||||||
Backend conventions
|
|
||||||
- Use English for code identifiers/messages; keep “pont-bascule” as domain term.
|
|
||||||
- API Platform operations are defined on Doctrine entities.
|
|
||||||
- Reception entity is in `src/Entity/Reception.php`, with custom weigh endpoint `/receptions/weigh`.
|
|
||||||
- Reception fields: `date_reception`, `license_plate`, `current_step` (default 0), `is_valid` (default false).
|
|
||||||
- Reception also has `identification_number` (auto `N-BR-####`), `merchandise_type`, `merchandise_detail`, `buildings` (M2M), and `pellet_buildings` (via `reception_pellet_building`).
|
|
||||||
- `date_reception` is set by the UI, stored as `DateTimeImmutable`, serialized as `Y-m-d`.
|
|
||||||
- Weight entity (`src/Entity/Weight.php`) is 1–N with Reception, each row stores `type` (`gross` or `tare`), `dsd`, `weight`, `weighed_at` (all nullable except `type`).
|
|
||||||
- Weigh endpoint `/receptions/weigh` returns `PontBasculeReading` with `dsd`, `weight`, `weighedAt` (formatted `Y-m-d`).
|
|
||||||
- Custom exception: `App\Exception\PontBasculeException` with French messages, mapped to 500 in provider.
|
|
||||||
- Parsing of pont-bascule payload is in `src/Service/PontBasculePayloadDecoder.php`.
|
|
||||||
- `config/reference.php` is auto-generated; keep it.
|
|
||||||
|
|
||||||
Frontend conventions
|
|
||||||
- Nuxt SSR disabled; Tailwind used.
|
|
||||||
- Layout in `frontend/layouts/default.vue`: max width `1050px`, header full width.
|
|
||||||
- Tailwind custom color palette is `primary` (e.g. `bg-primary-500`).
|
|
||||||
- Global font stack uses Helvetica via Tailwind (`font-sans`) and `frontend/assets/css/main.css`.
|
|
||||||
- API composable in `frontend/composables/useApi.ts` with `get/post/put/patch/delete` and default JSON/PATCH content types.
|
|
||||||
- API errors/success toasts can be customized via `toastErrorMessage`/`toastSuccessMessage` or i18n keys `toastErrorKey`/`toastSuccessKey`. Global method fallbacks use `errors.http.*` keys.
|
|
||||||
- `useApi` uses `useNuxtApp().$i18n` (not `useI18n`) to avoid setup-only constraint in service calls.
|
|
||||||
- Pinia store: `frontend/stores/reception.ts` is the source of truth for the current reception.
|
|
||||||
- Zod is used for form validation (e.g. `frontend/components/reception/reception-form.vue`); shared helpers live in `frontend/utils/zod-errors.ts`.
|
|
||||||
- Weighing logic is shared via `frontend/composables/useWeighing.ts`.
|
|
||||||
- Reception step UI uses store state (`currentStep`) in `frontend/pages/reception/[[id]].vue`.
|
|
||||||
- Step 2 uses `frontend/components/reception/reception-product-received.vue` for merchandise selection; type codes in `frontend/utils/constants.ts`.
|
|
||||||
- Active nav styles in header use `NuxtLink` with `custom` slot.
|
|
||||||
- Reusable UI components live under `frontend/components/ui/` and are auto-imported with `Ui` prefix (e.g. `UiLoadingDots`).
|
|
||||||
- Service layer lives in `frontend/services/` with typed DTOs in `frontend/services/dto/`.
|
|
||||||
- Reception service uses `receptions`, `receptions/{id}`, `receptions/weigh` and supports success/error toast keys.
|
|
||||||
- Reception receipt endpoint is `receptions/{id}/receipt` (PDF) via `frontend/composables/usePdfPrinter.ts`.
|
|
||||||
|
|
||||||
Environment & routing
|
|
||||||
- Frontend dev server: `npm run dev` in `frontend/`.
|
|
||||||
- API base for local dev: `http://localhost:8080/api` (set in `frontend/.env` via `NUXT_PUBLIC_API_BASE`).
|
|
||||||
- CORS handled by Nelmio; `.env` includes `CORS_ALLOW_ORIGIN` regex for localhost.
|
|
||||||
- Nuxt i18n locales live in `frontend/i18n/locales` (configured via `langDir: 'locales'`).
|
|
||||||
- Default locale is `fr`; translations in `frontend/i18n/locales/fr.json`.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
- Do not add a GET that creates resources; use POST + PATCH.
|
|
||||||
- Keep endpoints in plural (API Platform convention).
|
|
||||||
- New reference data added:
|
|
||||||
- Reception types (`reception_type`, fields: `label`, `code`), selectable on reception form.
|
|
||||||
- Merchandise types (`merchandise_type`, fields: `label`, `code`) and pellet types (`pellet_type`, fields: `label`, `code`).
|
|
||||||
- Buildings (`building`, fields: `label`, `code`) and reception allocations (`reception_building` M2M, `reception_pellet_building` unique on reception/pellet/building).
|
|
||||||
- Suppliers (`supplier`) with addresses (`address`, fields: `label`, `street`, `postal_code`, `city`, `country_code` ISO2), via `supplier_address` join table.
|
|
||||||
- Trucks (`truck`, field: `name`), linked to receptions.
|
|
||||||
- Carriers (`carrier`, fields: `name`, nullable `code`), Drivers (`driver`, fields: `name`, `carrier_id`), Vehicles (`vehicle`, fields: `plate`, `carrier_id`, `truck_id`) used for LIOT logic.
|
|
||||||
- Reception links: `reception_type_id`, `supplier_id`, `address_id`, `truck_id`, `carrier_id`, `driver_id`, `user_id`.
|
|
||||||
- Address exposes `fullAddress` via getter for display.
|
|
||||||
- LIOT behavior in reception form: if carrier code = `LIOT`, show driver + vehicle selects and hide manual license plate input; vehicle list filters by truck type and carrier; selected vehicle sets `license_plate`.
|
|
||||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -29,7 +29,37 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* Finalisation de la partie réception de marchandise
|
* Finalisation de la partie réception de marchandise
|
||||||
* [#267] Lister les réceptions en attente
|
* [#267] Lister les réceptions en attente
|
||||||
* [#268] Lister les réceptions terminées
|
* [#268] Lister les réceptions terminées
|
||||||
|
* [#316] Admin liste des transporteurs
|
||||||
|
* [#312] Creation administration listing fournisseurs
|
||||||
|
* [#315] Creation page admin utilisateur
|
||||||
|
* [#317] Admin modification creation transporteur
|
||||||
|
* [#318] Affichage modification reception terminée
|
||||||
|
* [#320] Affichage modification reception terminée suite
|
||||||
|
* [#271] Créer une nouvelle expédition (étape 1)
|
||||||
|
* [#272] Créer une nouvelle expédition (étape 2)
|
||||||
|
* [#273] Créer une nouvelle expédition (étape 3)
|
||||||
|
* [#256] Créer une nouvelle réception (étape 3 - bovin)
|
||||||
|
* [#314] Création d'une page d'administration : listing des utilisateurs
|
||||||
|
* [#313] Admin modification creation fournisseur
|
||||||
|
* [#275] Lister les expéditions en attente
|
||||||
|
* [#276] Lister les expéditions terminées
|
||||||
|
* [#324] Creation page admin listing clients
|
||||||
|
* [#326] Admin modification creation client
|
||||||
|
* [#325] Correction diverses
|
||||||
|
* fix layout admin
|
||||||
|
* Creation page admin listing bovins
|
||||||
|
* Creation page admin ajout/modification bovins
|
||||||
|
* [#331] Mettre à jour l'entité Shipment et bovin_shipment
|
||||||
|
* [#278] Plan du site
|
||||||
|
* [#334] Correctifs
|
||||||
|
* [#332] Refonte écran réception terminée
|
||||||
|
* [#327] afficher/modifier écran expédition terminée
|
||||||
|
* [#352] modification front admin fournisseur
|
||||||
|
* [#355] modification front admin transporteur
|
||||||
|
* [#356] front page admin bovin
|
||||||
|
* [#353] modification front admin client
|
||||||
|
* [#353] modification front admin utilisateur
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
127
CLAUDE.md
Normal file
127
CLAUDE.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Backend:** Symfony 8 + API Platform 4 (PHP 8.4)
|
||||||
|
- **Frontend:** Nuxt 4 (Vue 3, Pinia, Tailwind, Zod) in `frontend/`
|
||||||
|
- **Infra:** Docker (PHP-FPM + Nginx), Apache vhost serves API sous `/api` et frontend depuis `frontend/dist`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
make start # Démarrer les containers
|
||||||
|
make stop # Arrêter les containers
|
||||||
|
make restart # Redémarrer les containers
|
||||||
|
make shell # Shell dans le container PHP
|
||||||
|
|
||||||
|
# Install complet
|
||||||
|
make install # composer install + migrations + build frontend
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
make composer-install # Installer dépendances PHP
|
||||||
|
make migration-migrate # Lancer les migrations
|
||||||
|
make fixtures # Charger les fixtures
|
||||||
|
make cache-clear # Vider le cache Symfony
|
||||||
|
make test # Lancer les tests PHPUnit
|
||||||
|
make test FILES=tests/path/to/TestFile.php # Test spécifique
|
||||||
|
make php-cs-fixer-allow-risky FILES=src/... # Fixer le style
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
make build-nuxtJS # npm install + build:dist (dans le container)
|
||||||
|
make dev-nuxt # Serveur dev Nuxt (dans le container)
|
||||||
|
# Ou directement dans frontend/ :
|
||||||
|
cd frontend && npm run dev # Dev server (port 3000)
|
||||||
|
cd frontend && npm run build:dist # Build production
|
||||||
|
|
||||||
|
# Base de données
|
||||||
|
make db-reset # ⚠️ Supprime et recrée la BDD + migrations + fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture backend
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── ApiResource/ # Ressources API Platform custom
|
||||||
|
├── Command/ # Commandes Symfony (dont app:seed)
|
||||||
|
├── DataFixtures/ # Fixtures Doctrine
|
||||||
|
├── Dto/ # DTOs (ex: PontBasculeReading)
|
||||||
|
├── Entity/ # Entités Doctrine (= ressources API Platform)
|
||||||
|
├── Exception/ # Exceptions custom (PontBasculeException)
|
||||||
|
├── Kernel.php
|
||||||
|
├── Service/ # Services métier (PontBasculePayloadDecoder…)
|
||||||
|
└── State/ # State providers/processors API Platform
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture frontend
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── components/
|
||||||
|
│ ├── ui/ # Composants réutilisables, auto-importés avec préfixe Ui (ex: UiLoadingDots)
|
||||||
|
│ └── reception/ # Composants métier réception
|
||||||
|
├── composables/ # useApi, useWeighing, usePdfPrinter, useAppVersion
|
||||||
|
├── services/ # Couche service avec DTOs typés dans services/dto/
|
||||||
|
├── stores/ # Pinia stores (reception, shipment, auth)
|
||||||
|
├── pages/ # Pages Nuxt (file-based routing)
|
||||||
|
├── layouts/ # Layout default : max-width 1050px
|
||||||
|
├── i18n/locales/ # Traductions (défaut: fr)
|
||||||
|
├── utils/ # Constants, zod-errors helpers
|
||||||
|
└── assets/css/ # Tailwind config, main.css (font Helvetica)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions backend
|
||||||
|
|
||||||
|
- Code en anglais ; "pont-bascule" est un terme métier conservé tel quel.
|
||||||
|
- Les opérations API Platform sont définies directement sur les entités Doctrine.
|
||||||
|
- Pas de classes Repository custom : utiliser `EntityManagerInterface` avec les repos par défaut.
|
||||||
|
- `config/reference.php` est auto-généré — ne pas modifier à la main.
|
||||||
|
- Endpoints toujours au pluriel (convention API Platform).
|
||||||
|
- Ne jamais créer de GET qui crée des ressources : utiliser POST + PATCH.
|
||||||
|
|
||||||
|
## Conventions frontend
|
||||||
|
|
||||||
|
- SSR désactivé. Tailwind avec palette custom `primary` (ex: `bg-primary-500`).
|
||||||
|
- `useApi` (`composables/useApi.ts`) : méthodes `get/post/put/patch/delete` avec content-types par défaut.
|
||||||
|
- Toasts personnalisables via `toastErrorMessage`/`toastSuccessMessage` ou clés i18n `toastErrorKey`/`toastSuccessKey`.
|
||||||
|
- Utilise `useNuxtApp().$i18n` (pas `useI18n`) pour fonctionner hors setup.
|
||||||
|
- Validation formulaires avec Zod ; helpers dans `utils/zod-errors.ts`.
|
||||||
|
- Nav active : `NuxtLink` avec slot `custom`.
|
||||||
|
- PDFs : `usePdfPrinter` (receipt réception, rapport poids cases).
|
||||||
|
|
||||||
|
## Domaine métier clé
|
||||||
|
|
||||||
|
### Réception (pesée pont-bascule)
|
||||||
|
- Entité principale `Reception` : `date_reception` (DateTimeImmutable, format `Y-m-d`), `identification_number` (auto `N-BR-####`), `current_step` (défaut 0), `is_valid` (défaut false).
|
||||||
|
- `Weight` (1-N avec Reception) : `type` (`gross`/`tare`), `dsd`, `weight`, `weighed_at`.
|
||||||
|
- Endpoint pesée : `/receptions/weigh` → `PontBasculeReading` (dsd, weight, weighedAt).
|
||||||
|
- Parsing payload pont-bascule : `Service/PontBasculePayloadDecoder.php`.
|
||||||
|
- Exception : `PontBasculeException` (messages en français, mappée 500).
|
||||||
|
- Store Pinia `reception.ts` = source de vérité pour la réception en cours.
|
||||||
|
- UI multi-étapes dans `pages/reception/[[id]].vue` basée sur `currentStep`.
|
||||||
|
|
||||||
|
### LIOT (transport)
|
||||||
|
- Si carrier code = `LIOT` : afficher sélecteurs driver + vehicle, masquer saisie plaque manuelle.
|
||||||
|
- Liste véhicules filtrée par type de camion et transporteur.
|
||||||
|
- Le véhicule sélectionné alimente `license_plate`.
|
||||||
|
|
||||||
|
### Bovins & infrastructure
|
||||||
|
- `Bovine` : `nationalNumber` (unique), `receivedWeight`, `arrivalDate`, `buildingCase` (ManyToOne).
|
||||||
|
- `BuildingCase` a `bovines` (OneToMany).
|
||||||
|
- Rapport PDF cases : `GET /building_cases/{id}/weights-report` → template Twig, projection depuis `arrivalDate`, gain journalier fixe `1.3 kg/jour`.
|
||||||
|
|
||||||
|
### Données de référence
|
||||||
|
- `ReceptionType`, `MerchandiseType`, `PelletType`, `Building`, `Supplier` (avec `Address` via join table), `Truck`, `Carrier`, `Driver`, `Vehicle`.
|
||||||
|
- `Address` expose `fullAddress` via getter.
|
||||||
|
|
||||||
|
### Seed & fixtures
|
||||||
|
- Commande `app:seed` : seed infrastructure (statut, building_layout, building_case, building_case_position) puis bovins.
|
||||||
|
- Utilise des flush intermédiaires pour que les queries find fonctionnent sur les records fraîchement créés.
|
||||||
|
- Fixtures : `BuildingInfrastructureFixtures` + `BovineFixtures` (via dépendances `AppFixtures`).
|
||||||
|
|
||||||
|
## Environnement
|
||||||
|
|
||||||
|
- API base dev : `http://localhost:8080/api` (via `NUXT_PUBLIC_API_BASE` dans `frontend/.env`)
|
||||||
|
- CORS : Nelmio, configurable via `CORS_ALLOW_ORIGIN` dans `.env`
|
||||||
|
- Locale par défaut : `fr` — traductions dans `frontend/i18n/locales/fr.json`
|
||||||
|
- Docker env : `docker/.env.docker` (défaut) avec override possible via `docker/.env.docker.local`
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.0.32'
|
app.version: '0.0.70'
|
||||||
|
|||||||
101
frontend/components/address.vue
Normal file
101
frontend/components/address.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validateForm">
|
||||||
|
<div class="flex items-center mb-11 justify-between relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
|
<Icon @click="goBack" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ props.address ? "Modification d'une adresse" : "Ajout d'une adresse" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-y-16 gap-x-[200px] mb-16">
|
||||||
|
<UiTextInput id="address-label" v-model="form.label" label="Libellé" />
|
||||||
|
<UiTextInput id="address-street" v-model="form.street" label="Rue" />
|
||||||
|
<UiTextInput id="address-street2" v-model="form.street2" label="Complément" />
|
||||||
|
<UiTextInput id="address-postalCode" v-model="form.postalCode" label="Code postal" />
|
||||||
|
<UiTextInput id="address-city" v-model="form.city" label="Ville" />
|
||||||
|
<UiTextInput id="address-country" v-model="form.countryCode" label="Pays (code)" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AddressPayload } from "~/services/address"
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type?: "supplier" | "customer",
|
||||||
|
address?: AddressPayload | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const emptyForm = (): AddressPayload => ({
|
||||||
|
label: "",
|
||||||
|
street: "",
|
||||||
|
street2: null,
|
||||||
|
postalCode: "",
|
||||||
|
city: "",
|
||||||
|
countryCode: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive<AddressPayload>(emptyForm())
|
||||||
|
|
||||||
|
const backPath = computed(() => {
|
||||||
|
if (props.type === "customer") {
|
||||||
|
const customerId = Number(route.query.customerId)
|
||||||
|
return Number.isFinite(customerId) && customerId > 0
|
||||||
|
? `/admin/customer/${customerId}`
|
||||||
|
: "/admin/customer/customer-list"
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierId = Number(route.query.supplierId)
|
||||||
|
return Number.isFinite(supplierId) && supplierId > 0
|
||||||
|
? `/admin/supplier/${supplierId}`
|
||||||
|
: "/admin/supplier/supplier-list"
|
||||||
|
})
|
||||||
|
|
||||||
|
const hydrateForm = (address?: AddressPayload | null) => {
|
||||||
|
const data = address ?? emptyForm()
|
||||||
|
form.label = data.label ?? ""
|
||||||
|
form.street = data.street ?? ""
|
||||||
|
form.street2 = data.street2 ?? null
|
||||||
|
form.postalCode = data.postalCode ?? ""
|
||||||
|
form.city = data.city ?? ""
|
||||||
|
form.countryCode = data.countryCode ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.address,
|
||||||
|
(addr) => {
|
||||||
|
hydrateForm(addr)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
if (isLoading.value) return
|
||||||
|
emit("validate", {...form})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push(backPath.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'validate', form: AddressPayload): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink :to="link">
|
<NuxtLink :to="link">
|
||||||
<div class="w-[324px] h-[228px] border border-black rounded-md p-6 flex flex-col justify-between">
|
<div class="w-[300px] h-[216px] border border-primary-700 rounded-lg p-6 flex flex-col justify-between gap-4">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="rounded-full w-[80px] h-[80px] bg-neutral-400 flex justify-center items-center">
|
<div class="rounded-full w-[80px] h-[80px] bg-[#D9D9D9] flex justify-center items-center">
|
||||||
<Icon :name="iconName" style="color: black" size="44" />
|
<Icon :name="iconName" class="!text-primary-700" size="44" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Icon name="mdi:plus" style="color: black" size="44" />
|
<Icon name="mdi:plus" style="color: black" size="44" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uppercase font-bold">
|
<div class="uppercase font-bold">
|
||||||
<p class="text-3xl"> {{ label }} </p>
|
<p class="text-3xl text-primary-700">
|
||||||
|
<slot name="label">{{ label }}</slot>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -27,4 +27,3 @@ const props = defineProps<{
|
|||||||
label: string
|
label: string
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
62
frontend/components/commun/update-weight.vue
Normal file
62
frontend/components/commun/update-weight.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="grid grid-cols-3 gap-x-40 gap-y-8 mb-8">
|
||||||
|
<UiNumberInput
|
||||||
|
:key="localWeight.type"
|
||||||
|
:label="'POIDS'"
|
||||||
|
labelClass="font-bold uppercase text-xl "
|
||||||
|
v-model="localWeight.weight"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
:min="0"
|
||||||
|
wrapper-class="flex-col"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiDateInput
|
||||||
|
label="Date de pesée"
|
||||||
|
v-model="localWeight.weighedAt"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiNumberInput
|
||||||
|
label="Dsd"
|
||||||
|
class="col-start-2"
|
||||||
|
labelClass="font-bold uppercase"
|
||||||
|
v-model="localWeight.dsd"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
wrapper-class="flex-col"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {WeightEntryData} from '~/services/dto/weight-data'
|
||||||
|
import {reactive, watch} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: WeightEntryData
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: WeightEntryData): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localWeight = reactive<WeightEntryData>({...props.modelValue})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value) => {
|
||||||
|
Object.assign(localWeight, value)
|
||||||
|
},
|
||||||
|
{deep: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
localWeight,
|
||||||
|
(value) => {
|
||||||
|
emit('update:modelValue', {...value})
|
||||||
|
},
|
||||||
|
{deep: true}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -1,21 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
||||||
class="flex flex-col items-center gap-16">
|
class="flex flex-col gap-16">
|
||||||
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1>
|
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row gap-8 items-center">
|
class="flex flex-row gap-8 items-center w-full">
|
||||||
<div
|
<div
|
||||||
v-for="type in bovineType"
|
v-for="type in bovineType"
|
||||||
:key="type.id"
|
:key="type.id"
|
||||||
class="mt-8 flex flex-row mb-2 gap-6">
|
class="mt-8 flex flex-row mb-2 w-full">
|
||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
|
:id="type.id"
|
||||||
:label="type.label"
|
:label="type.label"
|
||||||
:code="type.code"
|
:code="type.code"
|
||||||
v-model="bovineQuantities[String(type.id)]"
|
v-model="bovineQuantities[String(type.id)]"
|
||||||
:placeholder="0"
|
:placeholder="0"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="10"
|
:max="10"
|
||||||
|
class="max-w-[150px]"
|
||||||
|
wrapper-class="gap-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -23,14 +26,19 @@
|
|||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
label="Autres"
|
label="Autres"
|
||||||
v-model="otherQuantity"
|
v-model="otherQuantity"
|
||||||
|
class="max-w-[80px]"
|
||||||
|
wrapper-class="gap-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex justify-center">
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
<UiButton
|
||||||
@click="goNext"
|
type="submit"
|
||||||
>Peser
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
</button>
|
@click="goNext"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -77,14 +85,14 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => receptionId.value,
|
[() => receptionId.value, () => bovineType.value],
|
||||||
async (id) => {
|
async ([id, types]) => {
|
||||||
if (!id || !receptionIri.value) {
|
if (!id || !receptionIri.value || types.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionMap: Record<string, number | null> = {}
|
const selectionMap: Record<string, number | null> = {}
|
||||||
for (const type of bovineType.value) {
|
for (const type of types) {
|
||||||
selectionMap[String(type.id)] = 0
|
selectionMap[String(type.id)] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
||||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Réception</h1>
|
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Réception</h1>
|
||||||
<!-- Nom de l'utilisateur -->
|
<!-- Nom de l'utilisateur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-user"
|
id="reception-user"
|
||||||
@@ -81,20 +81,8 @@
|
|||||||
select-class="h-[34px]"
|
select-class="h-[34px]"
|
||||||
wrapper-class="col-start-2 row-start-3"
|
wrapper-class="col-start-2 row-start-3"
|
||||||
/>
|
/>
|
||||||
<!-- Chauffeur (LIOT) -->
|
|
||||||
<UiSelect
|
|
||||||
id="reception-driver"
|
|
||||||
v-model="form.driverId"
|
|
||||||
label="Nom du chauffeur si LIOT"
|
|
||||||
:options="filteredDrivers.map((driver) => ({
|
|
||||||
value: String(driver.id),
|
|
||||||
label: driver.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingDrivers"
|
|
||||||
wrapper-class="col-start-2 row-start-4"
|
|
||||||
/>
|
|
||||||
<!-- Plaque d'immatriculation -->
|
<!-- Plaque d'immatriculation -->
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
@@ -112,17 +100,31 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingVehicles"
|
:loading="isLoadingVehicles"
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4 h-[64px]"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
v-if="isLiotCarrier"
|
||||||
wrapper-class="col-start-2 row-start-5"
|
wrapper-class="col-start-2 row-start-5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
>Peser
|
>Valider
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -142,21 +144,9 @@ import type {DriverData} from '~/services/dto/driver-data'
|
|||||||
import {getDriverList} from '~/services/driver'
|
import {getDriverList} from '~/services/driver'
|
||||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||||
import {getVehicleList} from '~/services/vehicle'
|
import {getVehicleList} from '~/services/vehicle'
|
||||||
import {RECEPTION_TYPE_CODES, SUPLLIER_CODE} from "~/utils/constants";
|
import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
|
||||||
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
||||||
|
import type {ReceptionFormData} from "~/services/dto/reception-data";
|
||||||
type ReceptionFormData = {
|
|
||||||
licensePlate: string
|
|
||||||
receptionDate: string
|
|
||||||
receptionTypeId: string
|
|
||||||
userId: string
|
|
||||||
supplierId: string
|
|
||||||
addressId: string
|
|
||||||
truckId: string
|
|
||||||
carrierId: string
|
|
||||||
driverId: string
|
|
||||||
vehicleId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
@@ -195,7 +185,7 @@ const selectedCarrier = computed(() =>
|
|||||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
)
|
)
|
||||||
// Indique si le transporteur est LIOT
|
// Indique si le transporteur est LIOT
|
||||||
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPLLIER_CODE.LIOT)
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
// Adresses disponibles pour le fournisseur sélectionné
|
// Adresses disponibles pour le fournisseur sélectionné
|
||||||
const supplierAddresses = computed(() => {
|
const supplierAddresses = computed(() => {
|
||||||
const supplierId = Number(form.supplierId)
|
const supplierId = Number(form.supplierId)
|
||||||
@@ -353,7 +343,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
watch(
|
watch(
|
||||||
() => [form.supplierId, suppliers.value],
|
() => [form.supplierId, form.addressId, suppliers.value],
|
||||||
() => {
|
() => {
|
||||||
if (!form.supplierId) {
|
if (!form.supplierId) {
|
||||||
form.addressId = ''
|
form.addressId = ''
|
||||||
@@ -370,7 +360,11 @@ watch(
|
|||||||
(address) => String(address.id) === form.addressId
|
(address) => String(address.id) === form.addressId
|
||||||
)
|
)
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
form.addressId = ''
|
if (supplierAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(supplierAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{immediate: true}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
||||||
class="flex flex-col gap-16 items-center w-full">
|
class="flex flex-col gap-16 items-center w-full">
|
||||||
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1>
|
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des marchandises réceptionnnées</h1>
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="merchandise-type"
|
id="merchandise-type"
|
||||||
v-model="selectedMerchandiseTypeId"
|
v-model="selectedMerchandiseTypeId"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
class="flex gap-4 w-[550px] justify-evenly"
|
class="flex gap-4 w-[550px] justify-between"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="building in buildings"
|
v-for="building in buildings"
|
||||||
@@ -47,28 +47,31 @@
|
|||||||
>
|
>
|
||||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
||||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||||
<p class="font-bold uppercase">{{ type.label }}</p>
|
<p class="font-bold uppercase text-primary-500">{{ type.label }}</p>
|
||||||
<div
|
<div
|
||||||
v-for="building in buildings"
|
v-for="building in buildings"
|
||||||
:key="building.id"
|
:key="building.id"
|
||||||
class="flex items-center gap-2 text-lg"
|
class="flex items-center gap-2 text-lg pl-[2px]"
|
||||||
>
|
>
|
||||||
<UiCheckbox
|
<UiCheckbox
|
||||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||||
:value="String(building.id)"
|
:value="String(building.id)"
|
||||||
:label="building.label"
|
:label="building.label"
|
||||||
label-class="text-lg"
|
label-class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex justify-center">
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
<UiButton
|
||||||
@click="goNext"
|
type="submit"
|
||||||
>Peser
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
</button>
|
@click="goNext"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1>
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
<p class="text-primary-500 uppercase text-2xl text-primary-500 mt-2">Pont-bascule connecté</p>
|
||||||
<div
|
<div
|
||||||
v-if="showLoadingBox"
|
v-if="!displayWeight"
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
<UiLoadingDots />
|
<UiLoadingDots />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="displayWeight !== null" class="w-full">
|
<div v-else class="w-full">
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
|
||||||
{{ displayWeight }} kg
|
{{ displayWeight }} kg
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center mt-[54px]">
|
<div class="flex justify-center mt-[54px]">
|
||||||
<button
|
<UiButton
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
@click="fetchWeight"
|
@click="fetchWeight"
|
||||||
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="displayWeight !== null && !showGenerateReceipt"
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="saveWeight"
|
@click="saveWeight"
|
||||||
>Valider la pesée</button>
|
>Valider la pesée</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="showGenerateReceipt"
|
v-if="showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="printReceipt"
|
@click="printReceipt"
|
||||||
>Générer le bon</button>
|
>Générer le bon</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import {computed} from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useWeighing } from '~/composables/useWeighing'
|
import { useWeighing } from '~/composables/useWeighing'
|
||||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
@@ -74,7 +74,9 @@ const printReceipt = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await saveWeight()
|
await saveWeight()
|
||||||
await printPdf(`/receptions/${receptionStore.current.id}/receipt`)
|
const reception = receptionStore.current
|
||||||
|
const filename = `${reception.identificationNumber ?? reception.id}_${reception.supplier?.name ?? 'fournisseur'}_${reception.licensePlate ?? 'immat'}.pdf`
|
||||||
|
await printPdf(`/receptions/${reception.id}/receipt`, filename)
|
||||||
|
|
||||||
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
@@ -90,10 +92,4 @@ const printReceipt = async () => {
|
|||||||
await router.push('/')
|
await router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupère le poids dès l'arrivée sur l'écran
|
|
||||||
onMounted(() => {
|
|
||||||
if (false === displayWeight.value) {
|
|
||||||
fetchWeight()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
161
frontend/components/reception/update-bovin.vue
Normal file
161
frontend/components/reception/update-bovin.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="flex flex-row justify-between gap-x-12 font-bold uppercase mb-8">
|
||||||
|
<div
|
||||||
|
v-for="type in bovineTypes"
|
||||||
|
:key="type.id"
|
||||||
|
>
|
||||||
|
<UiNumberInput
|
||||||
|
:label="type.label"
|
||||||
|
:code="type.code"
|
||||||
|
v-model="localQuantities[String(type.id)]"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
wrapperClass="w-44 flex-col"
|
||||||
|
inputClass="font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UiNumberInput
|
||||||
|
label="Autres"
|
||||||
|
v-model="localOtherQuantity"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
wrapperClass="w-44 flex-col"
|
||||||
|
inputClass="font-medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { getBovineTypeList } from '~/services/bovine-type'
|
||||||
|
import type { BovineTypeData } from '~/services/dto/bovine-type-data'
|
||||||
|
import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: ReceptionBovineTypeData[]
|
||||||
|
otherQuantity: number | null
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: ReceptionBovineTypeData[]): void
|
||||||
|
(event: 'update:otherQuantity', value: number | null): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const bovineTypes = ref<BovineTypeData[]>([])
|
||||||
|
const localQuantities = reactive<Record<string, number | null>>({})
|
||||||
|
const localOtherQuantity = ref<number | null>(props.otherQuantity ?? 0)
|
||||||
|
// Verrou pour éviter les boucles props -> local -> emit -> props.
|
||||||
|
const isSyncing = ref(false)
|
||||||
|
|
||||||
|
function entriesEqualByTypeAndQuantity(
|
||||||
|
left: ReceptionBovineTypeData[],
|
||||||
|
right: ReceptionBovineTypeData[]
|
||||||
|
): boolean {
|
||||||
|
const toMap = (entries: ReceptionBovineTypeData[]) => {
|
||||||
|
const map = new Map<number, number>()
|
||||||
|
for (const entry of entries) {
|
||||||
|
const typeId = entry.bovineType?.id ?? 0
|
||||||
|
map.set(typeId, entry.quantity ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = toMap(left)
|
||||||
|
const b = toMap(right)
|
||||||
|
if (a.size !== b.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [typeId, quantity] of a.entries()) {
|
||||||
|
if ((b.get(typeId) ?? 0) !== quantity) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEntriesFromLocal(): ReceptionBovineTypeData[] {
|
||||||
|
return bovineTypes.value.map((type) => {
|
||||||
|
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
|
||||||
|
return {
|
||||||
|
id: existing?.id ?? 0,
|
||||||
|
bovineType: type,
|
||||||
|
quantity: localQuantities[String(type.id)] ?? 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLocalFromProps() {
|
||||||
|
isSyncing.value = true
|
||||||
|
try {
|
||||||
|
for (const key of Object.keys(localQuantities)) {
|
||||||
|
delete localQuantities[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const type of bovineTypes.value) {
|
||||||
|
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
|
||||||
|
localQuantities[String(type.id)] = existing?.quantity ?? 0
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isSyncing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.otherQuantity,
|
||||||
|
(value) => {
|
||||||
|
if (isSyncing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = value ?? 0
|
||||||
|
isSyncing.value = true
|
||||||
|
localOtherQuantity.value = next
|
||||||
|
isSyncing.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(localOtherQuantity, (value) => {
|
||||||
|
if (isSyncing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = value ?? 0
|
||||||
|
emit('update:otherQuantity', next)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {
|
||||||
|
// Hydratation locale uniquement quand le parent change.
|
||||||
|
syncLocalFromProps()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
localQuantities,
|
||||||
|
() => {
|
||||||
|
if (isSyncing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// N'émet que si les quantités diffèrent réellement du parent.
|
||||||
|
const nextEntries = buildEntriesFromLocal()
|
||||||
|
if (!entriesEqualByTypeAndQuantity(nextEntries, props.modelValue)) {
|
||||||
|
emit('update:modelValue', nextEntries)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
bovineTypes.value = await getBovineTypeList()
|
||||||
|
syncLocalFromProps()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
269
frontend/components/reception/update-merchandise.vue
Normal file
269
frontend/components/reception/update-merchandise.vue
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-full col-start-1 row-start-1">
|
||||||
|
<UiRadioGroup
|
||||||
|
id="merchandise-type"
|
||||||
|
v-model="selectedMerchandiseTypeId"
|
||||||
|
label="Type de marchandises"
|
||||||
|
:options="merchandiseTypes.map((type) => ({
|
||||||
|
value: String(type.id),
|
||||||
|
label: type.label
|
||||||
|
}))"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
option-label-class="uppercase"
|
||||||
|
wrapper-class="w-full uppercase"
|
||||||
|
group-class="grid grid-cols-[336px_336px_355px_200px] w-[160px_160px_200px_180px] mt-9 mb-7"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full grid grid-cols-[3fr_1fr] gap-12 col-start-2 row-start-1">
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
|
class="flex gap-[218px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="building in buildings"
|
||||||
|
:key="building.id"
|
||||||
|
>
|
||||||
|
<UiCheckbox
|
||||||
|
v-model="selectedBuildingIds"
|
||||||
|
:value="String(building.id)"
|
||||||
|
:label="building.label"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
label-class="text-xl uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && isAutres"
|
||||||
|
class="flex flex-col justify-self-end max-w-[182px]"
|
||||||
|
>
|
||||||
|
<UiTextInput
|
||||||
|
id="merchandise-detail"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
v-model="merchandiseDetail"
|
||||||
|
placeholder="Préciser"
|
||||||
|
:maxlength="255"
|
||||||
|
class="h-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedMerchandiseTypeId && isGranule"
|
||||||
|
class="flex flex-col gap-10 w-full col-start-2 row-start-1"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-[max-content_max-content_max-content_max-content] justify-between">
|
||||||
|
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||||
|
<p class="mb-1 font-medium uppercase">{{ type.label }}</p>
|
||||||
|
<div
|
||||||
|
v-for="building in buildings"
|
||||||
|
:key="building.id"
|
||||||
|
class="flex text-lg"
|
||||||
|
>
|
||||||
|
<UiCheckbox
|
||||||
|
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||||
|
:value="String(building.id)"
|
||||||
|
:label="building.label"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
label-class="text-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import type { BuildingData } from '~/services/dto/building-data'
|
||||||
|
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||||
|
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
||||||
|
import type { MerchandiseEntryData } from '~/services/dto/reception-data'
|
||||||
|
import { getBuildingList } from '~/services/building'
|
||||||
|
import { getMerchandiseTypeList } from '~/services/merchandise-type'
|
||||||
|
import { getPelletTypeList } from '~/services/pellet-type'
|
||||||
|
import { MERCHANDISE_TYPE_CODES } from '~/utils/constants'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: MerchandiseEntryData
|
||||||
|
isAdmin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: MerchandiseEntryData): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
||||||
|
const buildings = ref<BuildingData[]>([])
|
||||||
|
const pelletTypes = ref<PelletTypeData[]>([])
|
||||||
|
|
||||||
|
const selectedMerchandiseTypeId = ref('')
|
||||||
|
const selectedBuildingIds = ref<string[]>([])
|
||||||
|
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||||
|
const merchandiseDetail = ref('')
|
||||||
|
// Verrou de synchro pour empêcher les aller-retours infinis entre parent et composant.
|
||||||
|
const isSyncing = ref(false)
|
||||||
|
const isReady = ref(false)
|
||||||
|
|
||||||
|
const selectedMerchandiseType = computed(() =>
|
||||||
|
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value) ?? null
|
||||||
|
)
|
||||||
|
const isGranule = computed(
|
||||||
|
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE
|
||||||
|
)
|
||||||
|
const isAutres = computed(
|
||||||
|
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES
|
||||||
|
)
|
||||||
|
|
||||||
|
function clonePelletSelections(value: Record<string, string[]>) {
|
||||||
|
const clone: Record<string, string[]> = {}
|
||||||
|
for (const [key, buildingIds] of Object.entries(value)) {
|
||||||
|
clone[key] = [...buildingIds]
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
function sorted(values: string[]): string[] {
|
||||||
|
return [...values].sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeModel(value: MerchandiseEntryData): MerchandiseEntryData {
|
||||||
|
// Normalisation stable pour comparer deux modèles sans faux positifs (ordre des tableaux).
|
||||||
|
const pellet: Record<string, string[]> = {}
|
||||||
|
const pelletKeys = Object.keys(value.selectedPelletBuildingIds ?? {}).sort()
|
||||||
|
for (const key of pelletKeys) {
|
||||||
|
pellet[key] = sorted(value.selectedPelletBuildingIds[key] ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
merchandiseTypeId: value.merchandiseTypeId ?? '',
|
||||||
|
merchandiseDetail: value.merchandiseDetail ?? '',
|
||||||
|
selectedBuildingIds: sorted(value.selectedBuildingIds ?? []),
|
||||||
|
selectedPelletBuildingIds: pellet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCurrentModel(): MerchandiseEntryData {
|
||||||
|
return {
|
||||||
|
merchandiseTypeId: selectedMerchandiseTypeId.value,
|
||||||
|
merchandiseDetail: merchandiseDetail.value,
|
||||||
|
selectedBuildingIds: [...selectedBuildingIds.value],
|
||||||
|
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameModel(left: MerchandiseEntryData, right: MerchandiseEntryData): boolean {
|
||||||
|
return JSON.stringify(normalizeModel(left)) === JSON.stringify(normalizeModel(right))
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePelletKeys() {
|
||||||
|
for (const pelletType of pelletTypes.value) {
|
||||||
|
const key = String(pelletType.id)
|
||||||
|
if (!selectedPelletBuildingIds.value[key]) {
|
||||||
|
selectedPelletBuildingIds.value[key] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateFromModelValue(value: MerchandiseEntryData) {
|
||||||
|
isSyncing.value = true
|
||||||
|
try {
|
||||||
|
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
|
||||||
|
merchandiseDetail.value = value.merchandiseDetail ?? ''
|
||||||
|
selectedBuildingIds.value = [...(value.selectedBuildingIds ?? [])]
|
||||||
|
selectedPelletBuildingIds.value = clonePelletSelections(
|
||||||
|
value.selectedPelletBuildingIds ?? {}
|
||||||
|
)
|
||||||
|
ensurePelletKeys()
|
||||||
|
} finally {
|
||||||
|
isSyncing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLocalState() {
|
||||||
|
if (isGranule.value) {
|
||||||
|
if (selectedBuildingIds.value.length > 0) {
|
||||||
|
selectedBuildingIds.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const key of Object.keys(selectedPelletBuildingIds.value)) {
|
||||||
|
if (selectedPelletBuildingIds.value[key].length > 0) {
|
||||||
|
selectedPelletBuildingIds.value[key] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAutres.value && merchandiseDetail.value !== '') {
|
||||||
|
merchandiseDetail.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitCurrentModel() {
|
||||||
|
const currentModel = buildCurrentModel()
|
||||||
|
// Ne pas réémettre si rien n'a changé côté métier.
|
||||||
|
if (isSameModel(currentModel, props.modelValue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', currentModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value) => {
|
||||||
|
const currentModel = buildCurrentModel()
|
||||||
|
// Si local == parent, on ignore pour éviter la boucle de réhydratation.
|
||||||
|
if (isSameModel(currentModel, value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hydrateFromModelValue(value)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
|
||||||
|
() => {
|
||||||
|
if (isSyncing.value || !isReady.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeSanitize = buildCurrentModel()
|
||||||
|
isSyncing.value = true
|
||||||
|
// Applique les règles métier (granulé / autres) avant émission.
|
||||||
|
sanitizeLocalState()
|
||||||
|
isSyncing.value = false
|
||||||
|
|
||||||
|
const afterSanitize = buildCurrentModel()
|
||||||
|
// Si la sanitation a modifié l'état, on laisse le watcher repasser proprement.
|
||||||
|
if (!isSameModel(beforeSanitize, afterSanitize)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emitCurrentModel()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
||||||
|
getMerchandiseTypeList(),
|
||||||
|
getBuildingList(),
|
||||||
|
getPelletTypeList()
|
||||||
|
])
|
||||||
|
merchandiseTypes.value = merchandiseTypeList
|
||||||
|
buildings.value = buildingList
|
||||||
|
pelletTypes.value = pelletTypeList
|
||||||
|
|
||||||
|
hydrateFromModelValue(props.modelValue)
|
||||||
|
isReady.value = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
554
frontend/components/shipment/shipment-form.vue
Normal file
554
frontend/components/shipment/shipment-form.vue
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
||||||
|
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Expédition</h1>
|
||||||
|
<!-- Nom de l'utilisateur -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-user"
|
||||||
|
v-model="form.userId"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:options="users.map((user) => ({
|
||||||
|
value: String(user.id),
|
||||||
|
label: user.username
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingUsers"
|
||||||
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
/>
|
||||||
|
<!-- Date de l'éxpedition -->
|
||||||
|
<UiDateInput
|
||||||
|
id="shipment-date"
|
||||||
|
v-model="form.shipmentDate"
|
||||||
|
label="Date du jour"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- Type d'expédition -->
|
||||||
|
<div class="col-start-1 row-start-4 h-[64px]">
|
||||||
|
<div class="flex w-full items-end gap-[104px]">
|
||||||
|
<UiRadioGroup
|
||||||
|
id="shipment-type"
|
||||||
|
name="shipment-type"
|
||||||
|
label="Type d'expédition bovine"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
wrapper-class=""
|
||||||
|
group-class="flex flex-row gap-[104px] w-[160px_160px] h-[32px]"
|
||||||
|
v-model="selectedShipmentTypeId"
|
||||||
|
:options="bovineShipment.map((type) => ({
|
||||||
|
value: String(type.id),
|
||||||
|
label: type.label
|
||||||
|
}))"
|
||||||
|
/>
|
||||||
|
<UiNumberInput
|
||||||
|
id="shipment-type-quantity"
|
||||||
|
v-model="shipmentQuantity"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="1200"
|
||||||
|
:disabled="!selectedShipmentTypeId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Client -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-customer"
|
||||||
|
v-model="form.customerId"
|
||||||
|
label="Client"
|
||||||
|
:options="customers.map((customer) => ({
|
||||||
|
value: String(customer.id),
|
||||||
|
label: customer.name || `Client #${customer.id}`
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingCustomers"
|
||||||
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
/>
|
||||||
|
<!-- Adresse du client -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
:options="customerAddressOptions"
|
||||||
|
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||||
|
label="Adresse"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
<!-- Camion -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-truck"
|
||||||
|
v-model="form.truckId"
|
||||||
|
label="Camion"
|
||||||
|
:options="trucks.map((truck) => ({
|
||||||
|
value: String(truck.id),
|
||||||
|
label: truck.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingTrucks"
|
||||||
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
/>
|
||||||
|
<!-- Transporteur -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-carrier"
|
||||||
|
v-model="form.carrierId"
|
||||||
|
label="Transporteur"
|
||||||
|
:options="carriers.map((carrier) => ({
|
||||||
|
value: String(carrier.id),
|
||||||
|
label: carrier.name
|
||||||
|
}))"
|
||||||
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- Plaque d'immatriculation (hors LIOT) -->
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Immatriculation (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
id="shipment-vehicle"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
wrapper-class="col-start-2 row-start-5"
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {UserData} from '~/services/dto/user-data'
|
||||||
|
import type {CustomerData} from '~/services/dto/customer-data'
|
||||||
|
import type {TruckData} from '~/services/dto/truck-data'
|
||||||
|
import type {CarrierData} from '~/services/dto/carrier-data'
|
||||||
|
import type {DriverData} from '~/services/dto/driver-data'
|
||||||
|
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||||
|
import type {AddressData} from '~/services/dto/address-data'
|
||||||
|
import {getUsers} from '~/services/auth'
|
||||||
|
import {getCustomerList} from '~/services/customer'
|
||||||
|
import {getTruckList} from '~/services/truck'
|
||||||
|
import {getCarrierList} from '~/services/carrier'
|
||||||
|
import {getVehicleList} from '~/services/vehicle'
|
||||||
|
import {getDriverList} from '~/services/driver'
|
||||||
|
import type {ShipmentFormData} from '~/services/dto/shipment-data'
|
||||||
|
import {SUPPLIER_CODE} from "~/utils/constants"
|
||||||
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
import {useShipmentStore} from '~/stores/shipment'
|
||||||
|
import {computed, reactive, ref, watch, onMounted} from 'vue'
|
||||||
|
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
|
||||||
|
import {getShipmentTypeList} from "~/services/shipment-type";
|
||||||
|
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const customers = ref<CustomerData[]>([])
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingShipmentTypes = ref(false)
|
||||||
|
const isLoadingCustomers = ref(false)
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
|
const selectedShipmentTypeId = ref('')
|
||||||
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
|
// Transporteur sélectionné dans le formulaire
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
|
||||||
|
const form = reactive<ShipmentFormData>({
|
||||||
|
userId: '',
|
||||||
|
shipmentDate: new Date().toISOString().slice(0, 10),
|
||||||
|
customerId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: '',
|
||||||
|
licensePlate: '',
|
||||||
|
})
|
||||||
|
// Adresses liées au client sélectionné
|
||||||
|
const customerAddresses = computed<AddressData[]>(() => {
|
||||||
|
const customerId = Number(form.customerId)
|
||||||
|
if (!Number.isFinite(customerId)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return customers.value.find((customer) => customer.id === customerId)?.addresses ?? []
|
||||||
|
})
|
||||||
|
// Options pour le select des adresses du client
|
||||||
|
const customerAddressOptions = computed(() =>
|
||||||
|
customerAddresses.value
|
||||||
|
.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
// Chauffeurs liés au transporteur sélectionné (LIOT)
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
// Véhicules liés au transporteur + camion sélectionnés (LIOT)
|
||||||
|
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return vehicles.value.filter(
|
||||||
|
(vehicle) =>
|
||||||
|
String(vehicle.carrier?.id) === form.carrierId &&
|
||||||
|
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// Chargement des données pour les selects
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadShipmentType = async () => {
|
||||||
|
isLoadingShipmentTypes.value = true
|
||||||
|
try {
|
||||||
|
bovineShipment.value = await getShipmentTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingShipmentTypes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCustomers = async () => {
|
||||||
|
isLoadingCustomers.value = true
|
||||||
|
try {
|
||||||
|
customers.value = await getCustomerList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCustomers.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const loadTrucks = async () => {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadCarriers = async () => {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// On met le user connecté par défaut dans le select
|
||||||
|
const setDefaultUser = () => {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Chargement initial des données
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadShipmentType()
|
||||||
|
await loadUsers()
|
||||||
|
await loadCustomers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadVehicles()
|
||||||
|
await loadDrivers()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
})
|
||||||
|
// Hydrate le formulaire depuis l'expédition en cours
|
||||||
|
watch(
|
||||||
|
() => shipmentStore.current,
|
||||||
|
(shipment) => {
|
||||||
|
isHydrating.value = true
|
||||||
|
form.licensePlate = shipment?.licensePlate ?? ''
|
||||||
|
form.shipmentDate = shipment?.shipmentDate ?? new Date().toISOString().slice(0, 10)
|
||||||
|
form.userId = shipment?.user?.id ? String(shipment.user.id) :
|
||||||
|
form.userId
|
||||||
|
form.customerId = shipment?.customer?.id ?
|
||||||
|
String(shipment.customer.id) : ''
|
||||||
|
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
|
||||||
|
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
|
||||||
|
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
|
||||||
|
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
|
||||||
|
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
|
||||||
|
|
||||||
|
|
||||||
|
selectedShipmentTypeId.value = shipment?.shipmentType?.id
|
||||||
|
? String(shipment.shipmentType.id)
|
||||||
|
: ''
|
||||||
|
|
||||||
|
shipmentQuantity.value = shipment?.nbBovinSend ?? 0
|
||||||
|
|
||||||
|
|
||||||
|
isHydrating.value = false
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.customerId, form.addressId, customers.value],
|
||||||
|
() => {
|
||||||
|
if (!form.customerId) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = customerAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
||||||
|
const applyLiotDefaults = () => {
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.carrierId) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredDrivers.value.length === 1) {
|
||||||
|
form.driverId = String(filteredDrivers.value[0].id)
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
applyLiotDefaults()
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => isHydrating.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) {
|
||||||
|
applyLiotDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.truckId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = filteredVehicles.value.some(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
form.vehicleId = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selected = filteredVehicles.value.find(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
form.licensePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, form.vehicleId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value || form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const match = filteredVehicles.value.find(
|
||||||
|
(vehicle) => vehicle.plate === form.licensePlate
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
form.vehicleId = String(match.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const buildPayload = () => {
|
||||||
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
|
const normalizedShipmentDate = form.shipmentDate.trim()
|
||||||
|
const normalizedCustomerId = form.customerId.trim()
|
||||||
|
const normalizedTruckId = form.truckId.trim()
|
||||||
|
const normalizedCarrierId = form.carrierId.trim()
|
||||||
|
const normalizedDriverId = form.driverId.trim()
|
||||||
|
const normalizedUserId = form.userId.trim()
|
||||||
|
const normalizedAddressId = form.addressId.trim()
|
||||||
|
const customerIri = normalizedCustomerId
|
||||||
|
? `/api/customers/${normalizedCustomerId}`
|
||||||
|
: null
|
||||||
|
const truckIri = normalizedTruckId
|
||||||
|
? `/api/trucks/${normalizedTruckId}`
|
||||||
|
: null
|
||||||
|
const carrierIri = normalizedCarrierId
|
||||||
|
? `/api/carriers/${normalizedCarrierId}`
|
||||||
|
: null
|
||||||
|
const userIri = normalizedUserId
|
||||||
|
? `/api/users/${normalizedUserId}`
|
||||||
|
: null
|
||||||
|
const driverIri = normalizedDriverId
|
||||||
|
? `/api/drivers/${normalizedDriverId}`
|
||||||
|
: null
|
||||||
|
const addressIri = normalizedAddressId
|
||||||
|
? `/api/addresses/${normalizedAddressId}`
|
||||||
|
: null
|
||||||
|
const normalizedShipmentTypeId = selectedShipmentTypeId.value.trim()
|
||||||
|
const shipmentTypeIri = normalizedShipmentTypeId
|
||||||
|
? `/api/shipment_types/${normalizedShipmentTypeId}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
const rawQuantity = Number(shipmentQuantity.value ?? 0)
|
||||||
|
const normalizedQuantity = Number.isFinite(rawQuantity) ? Math.max(0,
|
||||||
|
Math.trunc(rawQuantity)) : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
licensePlate: normalizedLicensePlate,
|
||||||
|
shipmentDate: normalizedShipmentDate,
|
||||||
|
customer: customerIri,
|
||||||
|
truck: truckIri,
|
||||||
|
carrier: carrierIri,
|
||||||
|
driver: driverIri,
|
||||||
|
user: userIri,
|
||||||
|
address: addressIri,
|
||||||
|
shipmentType: shipmentTypeIri,
|
||||||
|
nbBovinSend: normalizedQuantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDraft = async () => {
|
||||||
|
const payload = buildPayload()
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
await shipmentStore.createShipment({
|
||||||
|
currentStep: 0,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: shipmentStore.current.currentStep,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({saveDraft})
|
||||||
|
// Valide le formulaire et crée/met à jour l'expédition
|
||||||
|
const validate = async () => {
|
||||||
|
const payload = buildPayload()
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
const created = await shipmentStore.createShipment({
|
||||||
|
currentStep: 1,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
if (created) {
|
||||||
|
await shipmentStore.loadShipment(created.id)
|
||||||
|
await router.push(`/shipment/${created.id}`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextStep = shipmentStore.current.currentStep + 1
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
await shipmentStore.loadShipment(shipmentStore.current.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
26
frontend/components/shipment/shipment-loading.vue
Normal file
26
frontend/components/shipment/shipment-loading.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center gap-[150px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase text-primary-500">Chargement des bovins</h1>
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="goNext"
|
||||||
|
>Peser</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useShipmentStore} from "~/stores/shipment";
|
||||||
|
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
|
||||||
|
const goNext = async () => {
|
||||||
|
const nextStep = shipmentStore.current.currentStep + 1
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: nextStep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
95
frontend/components/shipment/shipment-weight.vue
Normal file
95
frontend/components/shipment/shipment-weight.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
|
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
||||||
|
<div
|
||||||
|
v-if="!displayWeight"
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<div v-else class="w-full">
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-primary-500 h-[90px] mt-12 mb-[86px] text-4xl text-primary-500">
|
||||||
|
{{ displayWeight }} kg
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-[54px]">
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
@click="fetchWeight"
|
||||||
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="saveWeight"
|
||||||
|
>Valider la pesée</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="printReceipt"
|
||||||
|
>Générer le bon</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useWeighingShipment } from '~/composables/useWeighing'
|
||||||
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
|
import { useShipmentStore } from '~/stores/shipment'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const { current: storeShipment } = storeToRefs(shipmentStore)
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
const {
|
||||||
|
displayWeight,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
} = useWeighingShipment({
|
||||||
|
modeShipment: props.mode,
|
||||||
|
shipment: storeShipment,
|
||||||
|
updateShipment: shipmentStore.updateShipment,
|
||||||
|
loadShipment: shipmentStore.loadShipment
|
||||||
|
})
|
||||||
|
// Affiche le bouton de génération du bon à l'étape tare
|
||||||
|
const showGenerateReceipt = computed(
|
||||||
|
() => props.mode === 'tare' && displayWeight.value !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
// Génère le bon d'expédition, puis clôture l'expédition
|
||||||
|
const printReceipt = async () => {
|
||||||
|
if (!import.meta.client || !shipmentStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWeight()
|
||||||
|
const shipment = shipmentStore.current
|
||||||
|
const filename = `${shipment.identificationNumber ?? shipment.id}_${shipment.customer?.label ?? 'client'}_${shipment.licensePlate ?? 'immat'}.pdf`
|
||||||
|
await printPdf(`/shipments/${shipment.id}/receipt`, filename)
|
||||||
|
|
||||||
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
const result = await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
isValid: true
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shipmentStore.clearCurrent()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
39
frontend/components/ui/UiButton.vue
Normal file
39
frontend/components/ui/UiButton.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="'button'"
|
||||||
|
:type="type"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
class="inline-flex min-w-[194px] items-center justify-center rounded-md"
|
||||||
|
:class="[
|
||||||
|
isDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
|
||||||
|
buttonClass
|
||||||
|
]"
|
||||||
|
v-bind="attrs"
|
||||||
|
>
|
||||||
|
<slot v-if="!loading" />
|
||||||
|
<UiLoadingDots v-else />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, useAttrs} from 'vue'
|
||||||
|
|
||||||
|
defineOptions({inheritAttrs: false})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
type?: 'button' | 'submit' | 'reset'
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
buttonClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
buttonClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const isDisabled = computed(() => props.disabled || props.loading)
|
||||||
|
</script>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<label
|
<label
|
||||||
class="flex items-center gap-2"
|
class="flex items-center gap-2 cursor-pointer text-primary-700"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="checked"
|
:checked="checked"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="inputClass"
|
:class="['h-4 w-4 cursor-pointer text-primary-500', inputClass]"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<span v-if="label">{{ label }}</span>
|
<span v-if="label">{{ label }}</span>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="font-bold uppercase text-xl mb-2"
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
:value="modelValue ?? ''"
|
:value="modelValue ?? ''"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
||||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
inputClass
|
inputClass
|
||||||
]"
|
]"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
// flex row passer en class wraper class flex col ainsi que le wfull 34
|
||||||
<template>
|
<template>
|
||||||
<div :class="['flex flex-row items-center gap-2', wrapperClass]">
|
<div :class="['flex', wrapperClass]">
|
||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="text-xl text-bold flex items-center gap-2"
|
class="text-xl flex items-center gap-2 text-primary-700"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -11,7 +12,8 @@
|
|||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="code" class="text-neutral-600">
|
v-if="code"
|
||||||
|
class="text-neutral-600">
|
||||||
({{ code }})
|
({{ code }})
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
:step="step"
|
:step="step"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black text-xl bg-transparent w-48"
|
class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||||
@@ -73,14 +75,41 @@ const emit = defineEmits<{
|
|||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
const isEmpty = computed(() => props.modelValue === null || props.modelValue === undefined || props.modelValue === '')
|
const isEmpty = computed(() => props.modelValue === null || props.modelValue === undefined || props.modelValue === '')
|
||||||
|
|
||||||
|
const toNumberOrNull = (value: number | string | undefined) => {
|
||||||
|
if (value === undefined || value === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
|
}
|
||||||
|
|
||||||
const onInput = (event: Event) => {
|
const onInput = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
if (target.value === '') {
|
if (target.value === '') {
|
||||||
emit('update:modelValue', null)
|
emit('update:modelValue', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const numeric = Math.max(0, Number(target.value))
|
const parsed = Number(target.value)
|
||||||
emit('update:modelValue', Number.isNaN(numeric) ? null : numeric)
|
if (!Number.isFinite(parsed)) {
|
||||||
|
emit('update:modelValue', null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = toNumberOrNull(props.min)
|
||||||
|
const max = toNumberOrNull(props.max)
|
||||||
|
|
||||||
|
let numeric = parsed
|
||||||
|
if (min !== null) {
|
||||||
|
numeric = Math.max(min, numeric)
|
||||||
|
} else {
|
||||||
|
numeric = Math.max(0, numeric)
|
||||||
|
}
|
||||||
|
if (max !== null) {
|
||||||
|
numeric = Math.min(max, numeric)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.value = String(numeric)
|
||||||
|
emit('update:modelValue', numeric)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeydown = (event: KeyboardEvent) => {
|
const onKeydown = (event: KeyboardEvent) => {
|
||||||
|
|||||||
93
frontend/components/ui/UiRadioGroup.vue
Normal file
93
frontend/components/ui/UiRadioGroup.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['flex flex-col', wrapperClass]">
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
:aria-label="label || id || 'radio-group'"
|
||||||
|
:class="['flex items-center gap-6 mt-1', groupClass]"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-for="option in options"
|
||||||
|
:key="String(option.value)"
|
||||||
|
:for="`${id || 'radio'}-${option.value}`"
|
||||||
|
class="flex items-center gap-2 text-primary-700"
|
||||||
|
:class="itemClass"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`${id || 'radio'}-${option.value}`"
|
||||||
|
type="radio"
|
||||||
|
:name="name || id || 'radio-group'"
|
||||||
|
:value="String(option.value)"
|
||||||
|
:checked="String(modelValue ?? '') === String(option.value)"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="h-4 w-4 border-primary-700/50 text-primary-700 focus:ring-primary-700"
|
||||||
|
:class="[
|
||||||
|
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
inputClass
|
||||||
|
]"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
|
<span class="text-xl" :class="optionLabelClass">
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAttrs } from 'vue'
|
||||||
|
|
||||||
|
type RadioOption = {
|
||||||
|
value: string | number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
label?: string
|
||||||
|
modelValue: string | number | null | undefined
|
||||||
|
options: RadioOption[]
|
||||||
|
disabled?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
groupClass?: string
|
||||||
|
itemClass?: string
|
||||||
|
inputClass?: string
|
||||||
|
optionLabelClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
label: '',
|
||||||
|
disabled: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
groupClass: '',
|
||||||
|
itemClass: '',
|
||||||
|
inputClass: '',
|
||||||
|
optionLabelClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const onChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="font-bold uppercase text-xl mb-2"
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
:value="modelValue ?? ''"
|
:value="modelValue ?? ''"
|
||||||
:disabled="disabled || loading"
|
:disabled="disabled || loading"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
|
class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] bg-transparent"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-primary-700',
|
||||||
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
|
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
selectClass
|
selectClass
|
||||||
]"
|
]"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
class="text-black"
|
class="text-primary-700"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="font-bold uppercase text-xl mb-2"
|
class="font-bold uppercase text-xl text-primary-700"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
:maxlength="maxlength"
|
:maxlength="maxlength"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black text-xl pb-[6px] bg-transparent"
|
class="border-b border-black text-xl py-[6px] bg-transparent text-primary-700"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label :for="inputId" class="font-bold uppercase text-xl mb-2">{{ label }}</label>
|
<label :for="inputId" class="font-bold uppercase text-xl text-primary-500">{{ label }}</label>
|
||||||
<div class="flex items-end gap-8">
|
<div class="flex items-end gap-8">
|
||||||
<input
|
<input
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:maxlength="maxLength"
|
:maxlength="maxLength"
|
||||||
:placeholder="placeholderText"
|
:placeholder="placeholderText"
|
||||||
class="border-b border-black flex-1 min-w-0 text-xl uppercase h-[30px]"
|
class="border-b border-black flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
<UiCheckbox
|
<UiCheckbox
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(label, index) in labels"
|
v-for="(label, index) in labels"
|
||||||
:key="label"
|
:key="label"
|
||||||
class="absolute top-0 whitespace-nowrap"
|
class="absolute top-0 whitespace-nowrap text-primary-500"
|
||||||
:class="labelClass(index)"
|
:class="labelClass(index)"
|
||||||
:style="positionStyle(index)"
|
:style="positionStyle(index)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
import {useApi} from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
|
|
||||||
export const usePdfPrinter = () => {
|
export const usePdfPrinter = () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const receptionStore = useReceptionStore()
|
|
||||||
const currentReception = receptionStore.current
|
|
||||||
|
|
||||||
const printPdf = async (url: string): Promise<void> => {
|
const printPdf = async (url: string, filename = 'document.pdf'): Promise<void> => {
|
||||||
const blob = await api.getBlob(url);
|
const blob = await api.getBlob(url)
|
||||||
|
|
||||||
const pdfBlob = blob.type === 'application/pdf'
|
const pdfBlob = blob.type === 'application/pdf'
|
||||||
? blob
|
? blob
|
||||||
: new Blob([blob], { type: 'application/pdf' });
|
: new Blob([blob], { type: 'application/pdf' })
|
||||||
|
|
||||||
const blobUrl = URL.createObjectURL(pdfBlob);
|
const blobUrl = URL.createObjectURL(pdfBlob)
|
||||||
|
|
||||||
const filename = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}.pdf`;
|
const a = document.createElement('a')
|
||||||
|
a.href = blobUrl
|
||||||
const a = document.createElement('a');
|
a.download = filename
|
||||||
a.href = blobUrl;
|
a.style.display = 'none'
|
||||||
a.download = filename;
|
document.body.appendChild(a)
|
||||||
a.style.display = 'none';
|
a.click()
|
||||||
document.body.appendChild(a);
|
a.remove()
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
|
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
|
||||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import type {Ref} from 'vue'
|
|
||||||
import {computed, ref} from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
|
import type {WeightEntryData} from '~/services/dto/reception-data'
|
||||||
import type {WeightData} from '~/services/dto/weight-data'
|
import type {WeightData} from '~/services/dto/weight-data'
|
||||||
import {getWeight} from '~/services/reception'
|
import {getWeight} from '~/services/reception'
|
||||||
|
import {getWeightShipment} from '~/services/shipment'
|
||||||
import {createWeight, updateWeight} from '~/services/weight'
|
import {createWeight, updateWeight} from '~/services/weight'
|
||||||
|
import type {UseWeighingShipmentOptions, UseWeighingOptions} from '~/services/weight'
|
||||||
|
import type {WeightShipmentEntryData} from "~/services/dto/shipment-data";
|
||||||
|
|
||||||
export type WeighingMode = 'gross' | 'tare'
|
export type WeighingMode = 'gross' | 'tare'
|
||||||
|
|
||||||
type UseWeighingOptions = {
|
|
||||||
mode: WeighingMode
|
|
||||||
reception: Ref<ReceptionData | null>
|
|
||||||
updateReception: (id: number, payload: ReceptionPayload) => Promise<ReceptionData | null>
|
|
||||||
loadReception?: (id: number) => Promise<ReceptionData | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWeighing = ({
|
export const useWeighing = ({
|
||||||
mode,
|
mode,
|
||||||
reception,
|
reception,
|
||||||
updateReception,
|
updateReception,
|
||||||
loadReception
|
loadReception
|
||||||
}: UseWeighingOptions) => {
|
}: UseWeighingOptions) => {
|
||||||
const weightData = ref<WeightData | null>(null)
|
const weightData = ref<WeightData | null>(null)
|
||||||
const isFetching = ref(false)
|
const isFetching = ref(false)
|
||||||
|
|
||||||
@@ -31,9 +27,7 @@ export const useWeighing = ({
|
|||||||
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
||||||
const showLoadingBox = computed(
|
const showLoadingBox = computed(() => isFetching.value)
|
||||||
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
|
||||||
)
|
|
||||||
|
|
||||||
const fetchWeight = async () => {
|
const fetchWeight = async () => {
|
||||||
isFetching.value = true
|
isFetching.value = true
|
||||||
@@ -65,7 +59,7 @@ export const useWeighing = ({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await createWeight({
|
await createWeight({
|
||||||
reception: `api/receptions/${reception.value.id}`,
|
reception: `/api/receptions/${reception.value.id}`,
|
||||||
type: mode,
|
type: mode,
|
||||||
dsd: baseDsd,
|
dsd: baseDsd,
|
||||||
weight: baseWeight,
|
weight: baseWeight,
|
||||||
@@ -97,3 +91,85 @@ export const useWeighing = ({
|
|||||||
saveWeight
|
saveWeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useWeighingShipment = ({
|
||||||
|
modeShipment,
|
||||||
|
shipment,
|
||||||
|
updateShipment,
|
||||||
|
loadShipment
|
||||||
|
}: UseWeighingShipmentOptions) => {
|
||||||
|
const weightData = ref<WeightData | null>(null)
|
||||||
|
const isFetching = ref(false)
|
||||||
|
|
||||||
|
const currentWeightEntry = computed<WeightShipmentEntryData | null>(() => {
|
||||||
|
const weights = shipment.value?.weights ?? []
|
||||||
|
return weights.find((entry) => entry.type === modeShipment) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
|
const title = computed(() => (modeShipment === 'gross' ? 'Pesée à vide' : 'Pesée à plein'))
|
||||||
|
const showLoadingBox = computed(() => isFetching.value)
|
||||||
|
|
||||||
|
const fetchWeight = async () => {
|
||||||
|
isFetching.value = true
|
||||||
|
weightData.value = await getWeightShipment().finally(() => {
|
||||||
|
isFetching.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveWeight = async () => {
|
||||||
|
if (!shipment.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingEntry = currentWeightEntry.value
|
||||||
|
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
||||||
|
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
||||||
|
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||||
|
|
||||||
|
if (baseWeight === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEntry?.id) {
|
||||||
|
await updateWeight(existingEntry.id, {
|
||||||
|
type: modeShipment,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createWeight({
|
||||||
|
shipment: `/api/shipments/${shipment.value.id}`,
|
||||||
|
type: modeShipment,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = modeShipment === 'tare'
|
||||||
|
? shipment.value.currentStep
|
||||||
|
: shipment.value.currentStep + 1
|
||||||
|
await updateShipment(shipment.value.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
isValid: shipment.value.isValid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loadShipment) {
|
||||||
|
await loadShipment(shipment.value.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export enum StepLabel {
|
export enum StepLabel {
|
||||||
Reception = 'Réception',
|
Reception = 'Réception',
|
||||||
GrossWeighing = 'Pesée à plein',
|
GrossWeighing = 'Pesée à plein',
|
||||||
Selection = 'Sélection réceptionnées',
|
Selection = 'Sélection réception',
|
||||||
TareWeighing = 'Pesée à vide'
|
TareWeighing = 'Pesée à vide',
|
||||||
|
Shipment = 'Expédition',
|
||||||
|
ShipmentLoading = 'Chargement',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RECEPTION_STEP_LABELS = [
|
export const RECEPTION_STEP_LABELS = [
|
||||||
@@ -11,3 +13,10 @@ export const RECEPTION_STEP_LABELS = [
|
|||||||
StepLabel.Selection,
|
StepLabel.Selection,
|
||||||
StepLabel.TareWeighing
|
StepLabel.TareWeighing
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const SHIPMENT_STEP_LABELS = [
|
||||||
|
StepLabel.Shipment,
|
||||||
|
StepLabel.TareWeighing,
|
||||||
|
StepLabel.ShipmentLoading,
|
||||||
|
StepLabel.GrossWeighing,
|
||||||
|
]
|
||||||
|
|||||||
@@ -12,8 +12,27 @@
|
|||||||
"fetch": "Impossible de récupérer la réception.",
|
"fetch": "Impossible de récupérer la réception.",
|
||||||
"create": "Impossible de créer la réception.",
|
"create": "Impossible de créer la réception.",
|
||||||
"update": "Impossible de mettre à jour la réception.",
|
"update": "Impossible de mettre à jour la réception.",
|
||||||
|
"weight": "Impossible de récupérer la pesée."
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"update": "Impossible de mettre à jour la pesée"
|
||||||
|
},
|
||||||
|
"shipment": {
|
||||||
|
"list": "Impossible de récupérer la liste des éxpeditions.",
|
||||||
|
"fetch": "Impossible de récupérer l'éxpeditions.",
|
||||||
|
"create": "Impossible de créer l'éxpeditions.",
|
||||||
|
"update": "Impossible de mettre à jour l'éxpeditions.",
|
||||||
"weigh": "Impossible de récupérer la pesée."
|
"weigh": "Impossible de récupérer la pesée."
|
||||||
},
|
},
|
||||||
|
"shipmentBovine": {
|
||||||
|
"list": "Impossible de récupérer la liste des bovins de l'éxpedition.",
|
||||||
|
"create": "Impossible d'enregistrer le bovin.",
|
||||||
|
"delete": "Impossible de supprimer le bovin.",
|
||||||
|
"update": "Impossible de mettre à jour le bovin."
|
||||||
|
},
|
||||||
|
"shipmentType": {
|
||||||
|
"list": "Impossible de récupérer la liste des types d'éxpedition."
|
||||||
|
},
|
||||||
"receptionType": {
|
"receptionType": {
|
||||||
"list": "Impossible de récupérer la liste des types de réception."
|
"list": "Impossible de récupérer la liste des types de réception."
|
||||||
},
|
},
|
||||||
@@ -37,16 +56,42 @@
|
|||||||
"delete": "Impossible de supprimer le bovin."
|
"delete": "Impossible de supprimer le bovin."
|
||||||
},
|
},
|
||||||
"supplier": {
|
"supplier": {
|
||||||
"list": "Impossible de récupérer la liste des fournisseurs."
|
"list": "Impossible de récupérer la liste des fournisseurs.",
|
||||||
|
"fetch": "Impossible de récupérer le fournisseur.",
|
||||||
|
"create": "Impossible de créer le fournisseur.",
|
||||||
|
"update": "Impossible de mettre à jour le fournisseur.",
|
||||||
|
"nameRequired": "Le nom du fournisseur est obligatoire."
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"fetch": "Impossible de récupérer l'adresse.",
|
||||||
|
"create": "Impossible de créer l'adresse.",
|
||||||
|
"update": "Impossible de mettre à jour l'adresse.",
|
||||||
|
"entityNotFound": "Entité introuvable.",
|
||||||
|
"streetRequired": "La rue est obligatoire.",
|
||||||
|
"postalCodeRequired": "Le code postal est obligatoire.",
|
||||||
|
"cityRequired": "La ville est obligatoire.",
|
||||||
|
"countryCodeInvalid": "Le pays doit être un code ISO2 (2 lettres)."
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"list": "Impossible de récupérer la liste des clients.",
|
||||||
|
"fetch": "Impossible de récupérer le client.",
|
||||||
|
"create": "Impossible de créer le client.",
|
||||||
|
"update": "Impossible de mettre à jour le client."
|
||||||
},
|
},
|
||||||
"truck": {
|
"truck": {
|
||||||
"list": "Impossible de récupérer la liste des camions."
|
"list": "Impossible de récupérer la liste des camions."
|
||||||
},
|
},
|
||||||
"bovin": {
|
"bovin": {
|
||||||
"list": "Impossible de récupérer la liste des races de bovins."
|
"list": "Impossible de récupérer la liste des races de bovins.",
|
||||||
|
"fetch": "Impossible de récupérer le type bovin.",
|
||||||
|
"create": "Impossible de créer le type bovin.",
|
||||||
|
"update": "Impossible de mettre à jour le type bovin."
|
||||||
},
|
},
|
||||||
"carrier": {
|
"carrier": {
|
||||||
"list": "Impossible de récupérer la liste des transporteurs."
|
"list": "Impossible de récupérer la liste des transporteurs.",
|
||||||
|
"fetch": "Impossible de récupérer les données du transporteur",
|
||||||
|
"update": "Impossible de mettre à jour le transporteur",
|
||||||
|
"create": "Impossible de créer le transporteur"
|
||||||
},
|
},
|
||||||
"driver": {
|
"driver": {
|
||||||
"list": "Impossible de récupérer la liste des chauffeurs."
|
"list": "Impossible de récupérer la liste des chauffeurs."
|
||||||
@@ -57,16 +102,48 @@
|
|||||||
"auth": {
|
"auth": {
|
||||||
"login": "Identifiants invalides.",
|
"login": "Identifiants invalides.",
|
||||||
"users": "Impossible de récupérer les utilisateurs.",
|
"users": "Impossible de récupérer les utilisateurs.",
|
||||||
"logout": "Impossible de se déconnecter."
|
"logout": "Impossible de se déconnecter.",
|
||||||
|
"update": "Impossible de mettre à jour l'utilisateur.",
|
||||||
|
"create": "Impossible de créer l'utilisateur."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"reception": {
|
"reception": {
|
||||||
|
"create": "Réception créée avec succès",
|
||||||
"update": "Réception mise à jour avec succès."
|
"update": "Réception mise à jour avec succès."
|
||||||
},
|
},
|
||||||
|
"shipment": {
|
||||||
|
"create": "Éxpedition créée avec succès",
|
||||||
|
"update": "Éxpedition mise à jour avec succès."
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"create": "Fournisseur créé avec succès.",
|
||||||
|
"update": "Fournisseur mis à jour avec succès."
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"create": "Client créé avec succès.",
|
||||||
|
"update": "Client mis à jour avec succès."
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"create": "Adresse créée avec succès.",
|
||||||
|
"update": "Adresse mise à jour avec succès."
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
|
"update": "Utilisateur mis à jour avec succès.",
|
||||||
|
"create": "Utilisateur créé avec succès.",
|
||||||
"login": "Connexion réussie.",
|
"login": "Connexion réussie.",
|
||||||
"logout": "Déconnexion réussie."
|
"logout": "Déconnexion réussie."
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"update": "Transporteur mis à jour",
|
||||||
|
"create": "Transporteur créé"
|
||||||
|
},
|
||||||
|
"bovin": {
|
||||||
|
"update": "Type bovin mis à jour avec succès.",
|
||||||
|
"create": "Type bovin créé avec succès."
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"update": "Pesée mis à jour"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-screen text-neutral-900 grid grid-rows-[85px,1fr]">
|
|
||||||
<!-- HEADER -->
|
|
||||||
<header class="bg-primary-500 z-50 h-[85px]">
|
|
||||||
<div class="h-full w-full px-6 grid grid-cols-[auto,1fr,auto] items-center gap-8">
|
|
||||||
<NuxtLink to="/" class="grid place-items-center">
|
|
||||||
<span class="grid place-items-center bg-white text-xl font-bold uppercase text-primary-500 p-4">
|
|
||||||
LOGO
|
|
||||||
</span>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<nav class="text-2xl font-bold uppercase text-white"></nav>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="text-xl font-bold uppercase text-white transition hover:opacity-80 justify-self-end"
|
|
||||||
>
|
|
||||||
Quitter le panel admin
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-[16rem,1fr] h-[calc(100vh-85px)] min-h-0">
|
|
||||||
<aside class="bg-primary-500 text-white min-h-0 grid grid-rows-[auto,1fr,auto]">
|
|
||||||
<div class="p-4 font-semibold">Tableau de bord</div>
|
|
||||||
|
|
||||||
<div class="overflow-y-auto min-h-0 p-4 space-y-3">
|
|
||||||
<!-- Liste des liens à ajouter ci-dessous -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4">
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
|
|
||||||
>
|
|
||||||
Déconnexion
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main class="min-h-0 overflow-auto px-12 py-12 ">
|
|
||||||
<div class="w-full ">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await auth.logout()
|
|
||||||
} finally {
|
|
||||||
await navigateTo('/login')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,50 +1,166 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-white text-neutral-900">
|
<div class="min-h-screen text-neutral-900 flex flex-col">
|
||||||
<header class="w-full border-b border-neutral-200 bg-primary-500">
|
<!-- HEADER -->
|
||||||
<div class="flex w-full items-center justify-center px-6 py-4">
|
<header class="w-full bg-primary-500 py-5 px-6">
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<!-- Burger (mobile) -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center justify-center text-3xl text-white md:hidden"
|
class="inline-flex items-center justify-center text-3xl text-white md:hidden"
|
||||||
aria-label="Ouvrir le menu"
|
aria-label="Ouvrir le menu"
|
||||||
@click="toggleMenu"
|
@click="toggleMenu"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" class="flex items-center"><Icon name="mdi:menu" size="44"/></span>
|
<span aria-hidden="true" class="flex items-center">
|
||||||
|
<Icon name="mdi:menu" size="44"/>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<nav class="ml-4 hidden items-center gap-8 text-2xl font-bold uppercase text-white md:flex">
|
|
||||||
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
|
<!-- Logo -->
|
||||||
|
<NuxtLink to="/" class="shrink-0">
|
||||||
|
<span class="flex items-center justify-center bg-white text-xl font-bold uppercase px-6 py-4">
|
||||||
|
LOGO
|
||||||
|
</span>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- NAV centré (desktop) -->
|
||||||
|
<nav
|
||||||
|
class="hidden md:flex flex-1 items-center justify-center gap-8 text-xl font-bold uppercase text-white"
|
||||||
|
>
|
||||||
|
<NuxtLink to="/" custom v-slot="{ href, navigate }">
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
:class="isExactActive ? 'opacity-100' : 'opacity-50'"
|
:class="route.path === '/'
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
>
|
>
|
||||||
Accueil
|
Accueil
|
||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/admin/dashboard" custom v-slot="{ href, navigate, isActive }">
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/user/list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/user')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
>
|
>
|
||||||
Admin
|
Utilisateurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/supplier/supplier-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/supplier')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Fournisseurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/carrier/carrier-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/carrier')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Transporteurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/customer/customer-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/customer')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Clients
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/bovin/bovin-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/bovin')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Bovins
|
||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
<NuxtLink to="/" class="flex flex-1 items-center justify-center gap-3">
|
|
||||||
<span
|
<!-- Spacer mobile (pour centrer visuellement le header si besoin) -->
|
||||||
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
|
||||||
>
|
|
||||||
LOGO
|
|
||||||
</span>
|
|
||||||
</NuxtLink>
|
|
||||||
<div class="w-[44px] md:hidden"></div>
|
<div class="w-[44px] md:hidden"></div>
|
||||||
<button
|
|
||||||
type="button"
|
<!-- User dropdown à droite (desktop) -->
|
||||||
class="ml-auto hidden text-xl font-bold uppercase text-white transition hover:opacity-80 md:inline-flex"
|
<div v-if="auth.isAuthenticated" class="ml-auto relative hidden md:flex items-center text-white group">
|
||||||
@click="handleLogout"
|
<button
|
||||||
>
|
type="button"
|
||||||
Déconnexion
|
class="inline-flex items-center py-2 -my-2 text-xl leading-none transition hover:opacity-80"
|
||||||
</button>
|
aria-haspopup="true"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:account-circle-outline" class="self-center" size="36"/>
|
||||||
|
<span class="capitalize font-bold ml-4">{{ userDisplayName }}</span>
|
||||||
|
<span
|
||||||
|
class="ml-[6px] inline-flex items-center font-bold transition-transform group-hover:rotate-180 group-focus-within:rotate-180">
|
||||||
|
<Icon name="mdi:chevron-down" size="20"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute right-0 top-full z-10 w-56 rounded-md bg-primary-500 py-2 border-neutral-300 border shadow-lg
|
||||||
|
opacity-0 invisible pointer-events-none transition
|
||||||
|
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto
|
||||||
|
group-focus-within:opacity-100 group-focus-within:visible group-focus-within:pointer-events-auto"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-4 py-2 text-left text-sm font-semibold text-white opacity-85 hover:opacity-100 transition"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay (mobile) -->
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition duration-200 ease-out"
|
enter-active-class="transition duration-200 ease-out"
|
||||||
enter-from-class="opacity-0"
|
enter-from-class="opacity-0"
|
||||||
@@ -59,6 +175,8 @@
|
|||||||
@click="closeMenu"
|
@click="closeMenu"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
|
<!-- Drawer (mobile) -->
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition duration-200 ease-out"
|
enter-active-class="transition duration-200 ease-out"
|
||||||
enter-from-class="-translate-x-full"
|
enter-from-class="-translate-x-full"
|
||||||
@@ -69,9 +187,7 @@
|
|||||||
>
|
>
|
||||||
<aside
|
<aside
|
||||||
v-if="isMenuOpen"
|
v-if="isMenuOpen"
|
||||||
class="fixed left-0 top-0 z-50 h-full w-full bg-primary-600 px-6 pb-8 pt-6 text-white shadow-xl md:hidden"
|
class="fixed left-0 top-0 z-50 h-full w-full bg-primary-500 px-6 pb-8 pt-6 text-white shadow-xl md:hidden"
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-2xl font-bold uppercase">Menu</span>
|
<span class="text-2xl font-bold uppercase">Menu</span>
|
||||||
@@ -84,12 +200,30 @@
|
|||||||
<Icon name="mdi:close" size="44"/>
|
<Icon name="mdi:close" size="44"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase">
|
<nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase">
|
||||||
<NuxtLink to="/" class="opacity-100" @click="closeMenu">Accueil</NuxtLink>
|
<NuxtLink to="/admin/dashboard" @click="closeMenu">Accueil</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/supplier/supplier-list" @click="closeMenu">
|
||||||
|
Fournisseurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/carrier/carrier-list" @click="closeMenu">
|
||||||
|
Transporteurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/user/list" @click="closeMenu">
|
||||||
|
Utilisateurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/customer/customer-list" @click="closeMenu">
|
||||||
|
Clients
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/bovin/bovin-list" @click="closeMenu">
|
||||||
|
Bovins
|
||||||
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="auth.isAuthenticated"
|
||||||
type="button"
|
type="button"
|
||||||
class="mt-5 text-xl font-bold uppercase"
|
class="mt-6 text-xl font-bold uppercase"
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
>
|
>
|
||||||
Déconnexion
|
Déconnexion
|
||||||
@@ -97,37 +231,40 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</transition>
|
</transition>
|
||||||
</header>
|
</header>
|
||||||
<main class="mx-auto w-full max-w-[1280px] pb-0">
|
<main class="mx-auto w-full max-w-[1280px] mt-16">
|
||||||
<slot/>
|
<slot/>
|
||||||
</main>
|
</main>
|
||||||
<footer class="w-full mt-8 bg-primary-500 p-6">
|
<footer class="w-full mt-auto bg-primary-500 px-6 py-3">
|
||||||
<p class="font-bold text-white text-right">v{{ version }}</p>
|
<p class="font-bold text-white text-right">v{{ version }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '~/stores/auth'
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const {version} = useAppVersion()
|
||||||
|
|
||||||
const isMenuOpen = ref(false)
|
const isMenuOpen = ref(false)
|
||||||
const { version } = useAppVersion()
|
|
||||||
|
const userDisplayName = computed(() => auth.user?.username ?? 'Utilisateur')
|
||||||
|
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
isMenuOpen.value = false
|
isMenuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleMenu = () => {
|
const toggleMenu = () => {
|
||||||
isMenuOpen.value = !isMenuOpen.value
|
isMenuOpen.value = !isMenuOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
} finally {
|
} finally {
|
||||||
closeMenu()
|
closeMenu()
|
||||||
await navigateTo('/login')
|
await navigateTo('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
110
frontend/pages/admin/bovin/[[id]].vue
Normal file
110
frontend/pages/admin/bovin/[[id]].vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="flex items-center justify-between relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
|
<Icon
|
||||||
|
@click="router.push('/admin/bovin/bovin-list')"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="40"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ route.params.id ? 'Modifications du type bovin' : 'Ajout d\'un type bovin' }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]">
|
||||||
|
<UiTextInput label="Nom du bovin" id="bovin-label" v-model="form.label" />
|
||||||
|
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || isHydrating"
|
||||||
|
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {createBovin, getBovin, updateBovin} from "~/services/bovine-type";
|
||||||
|
import type {BovineTypeData, BovinFormData} from "~/services/dto/bovine-type-data";
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const idBovin = computed(() => resolveId(route.params.id))
|
||||||
|
const isEdit = computed(() => idBovin.value !== null)
|
||||||
|
|
||||||
|
function resolveId(param: unknown) {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<BovinFormData>({
|
||||||
|
label: '',
|
||||||
|
code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const hydrateFromBovin = (bovin: BovineTypeData | null) => {
|
||||||
|
if (!bovin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.label = bovin.label ?? ''
|
||||||
|
form.code = bovin.code ?? ''
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => idBovin.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const bovin = await getBovin(id)
|
||||||
|
hydrateFromBovin(bovin)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value || isHydrating.value) return
|
||||||
|
|
||||||
|
const normalizedBovinCode = form.code.trim()
|
||||||
|
const normalizedBovinLabel = form.label.trim()
|
||||||
|
|
||||||
|
|
||||||
|
const basePayload = {
|
||||||
|
label: normalizedBovinLabel,
|
||||||
|
code: normalizedBovinCode
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
if (isEdit.value && idBovin.value !== null) {
|
||||||
|
await updateBovin(idBovin.value, basePayload)
|
||||||
|
} else {
|
||||||
|
await createBovin(basePayload)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigate(){
|
||||||
|
return router.push("/admin/bovin/list")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
67
frontend/pages/admin/bovin/bovin-list.vue
Normal file
67
frontend/pages/admin/bovin/bovin-list.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between ">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des types bovins</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mt-7 border border-slate-200 mb-11 ">
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-primary-700 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Code</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!auth.isAdmin" class="px-4 py-6 text-slate-400">
|
||||||
|
Accès réservé aux administrateurs.
|
||||||
|
</div>
|
||||||
|
<div v-else-if="bovinList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun type de bovin.
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="bovin in bovinList"
|
||||||
|
:key="bovin.id"
|
||||||
|
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToBovin(bovin.id)"
|
||||||
|
@keydown.enter="goToBovin(bovin.id)"
|
||||||
|
>
|
||||||
|
<div>{{ bovin.label }}</div>
|
||||||
|
<div>{{ bovin.code }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/bovin"
|
||||||
|
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getBovineTypeList } from "~/services/bovine-type"
|
||||||
|
import type { BovineTypeData } from "~/services/dto/bovine-type-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const bovinList = ref<BovineTypeData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToBovin = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/bovin/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
bovinList.value = await getBovineTypeList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
113
frontend/pages/admin/carrier/[[id]].vue
Normal file
113
frontend/pages/admin/carrier/[[id]].vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="flex items-center justify-between relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
|
<Icon
|
||||||
|
@click="router.push('/admin/carrier/carrier-list')"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="40"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ route.params.id ? 'Modification du transporteur' : 'Ajout d\'un transporteur' }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]">
|
||||||
|
<UiTextInput
|
||||||
|
label="Nom du transporteur"
|
||||||
|
id="carrier-name"
|
||||||
|
v-model="form.name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiTextInput
|
||||||
|
label="Code transporteur"
|
||||||
|
id="code-id"
|
||||||
|
v-model="form.code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier";
|
||||||
|
import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data";
|
||||||
|
import {computed} from "vue";
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const idCarrier = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<CarrierFormData>({
|
||||||
|
code:'',
|
||||||
|
name:''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hydrateFromUser = (carrier: CarrierData | null) => {
|
||||||
|
if (!carrier) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.name = carrier.name ?? ''
|
||||||
|
form.code = carrier.code ?? ''
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => idCarrier.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const user = await getCarrier(id)
|
||||||
|
hydrateFromUser(user)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
async function validate() {
|
||||||
|
|
||||||
|
const normalizedCarrierCode = form.code.trim()
|
||||||
|
const normalizedCarrierName = form.name.trim()
|
||||||
|
|
||||||
|
const basePayload = {
|
||||||
|
name: normalizedCarrierName,
|
||||||
|
code: normalizedCarrierCode
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idCarrier.value){
|
||||||
|
await updateCarrier(idCarrier.value, basePayload)
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
await createCarrier(basePayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(){
|
||||||
|
router.push("/admin/carrier/carrier-list")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
51
frontend/pages/admin/carrier/carrier-list.vue
Normal file
51
frontend/pages/admin/carrier/carrier-list.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between ">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">listes des transporteurs</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-7 border border-slate-200 mb-11 ">
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-primary-700 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Label</div>
|
||||||
|
<div>Code</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="carrier in carrierList"
|
||||||
|
:key="carrier.id"
|
||||||
|
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToCarrier(carrier.id)"
|
||||||
|
@keydown.enter="goToCarrier(carrier.id)"
|
||||||
|
>
|
||||||
|
<div>{{ carrier.name}}</div>
|
||||||
|
<div>{{ carrier.code }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/carrier"
|
||||||
|
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {CarrierData} from "~/services/dto/carrier-data";
|
||||||
|
import {getCarrierList} from "~/services/carrier";
|
||||||
|
|
||||||
|
const carrierList = ref<CarrierData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goToCarrier = (id: number) => {
|
||||||
|
router.push(`/admin/carrier/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
carrierList.value = await getCarrierList(false)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
202
frontend/pages/admin/customer/[[id]].vue
Normal file
202
frontend/pages/admin/customer/[[id]].vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
|
<Icon @click="router.push('/admin/customer/customer-list')" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ customerId ? "Modification du client" : "Ajout d'un client" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
|
<UiTextInput id="customer-name" v-model="form.name" label="Nom du client" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
<Icon :name="customerId ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ customerId ? "Valider" : "Ajouter" }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-7">
|
||||||
|
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du client</h2>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto mb-11 text-primary-700">
|
||||||
|
<table class="w-full border-collapse text-primary-700">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border bg-slate-100 border-gray-200">
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Libellé</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Rue</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Complément</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Code postal</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Ville</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Pays</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-if="form.addresses.length === 0">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="py-4 text-slate-400">
|
||||||
|
Aucune adresse.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr
|
||||||
|
v-for="(address, index) in form.addresses"
|
||||||
|
:key="address.id ?? index"
|
||||||
|
class="border border-gray-100 hover:bg-slate-50"
|
||||||
|
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="goToEditAddress(address.id ?? null)"
|
||||||
|
>
|
||||||
|
<td class="py-3 px-4">{{ address.label || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.street || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.street2 || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.postalCode || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.city || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.countryCode || "—" }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center text-xl gap-2 text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
:disabled="customerId === null || !auth.isAdmin"
|
||||||
|
@click="goToAddAddress"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
|
import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
|
||||||
|
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
|
||||||
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
const customerId = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const form = reactive<CustomerFormData>({
|
||||||
|
name: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
addresses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToAddAddress = () => {
|
||||||
|
if (customerId.value === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToEditAddress = (addressId: number | null) => {
|
||||||
|
if (customerId.value === null || addressId === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
addressId: String(addressId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFromCustomer = (customer: CustomerData | null) => {
|
||||||
|
if (!customer) return
|
||||||
|
form.name = customer.name ?? ""
|
||||||
|
form.phone = customer.phone ?? ""
|
||||||
|
form.email = customer.email ?? ""
|
||||||
|
if (!Array.isArray(customer.addresses) || customer.addresses.length === 0) {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof customer.addresses[0] === "string") {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addresses = customer.addresses.map((address) => ({
|
||||||
|
id: address.id ?? null,
|
||||||
|
label: address.label ?? "",
|
||||||
|
street: address.street ?? "",
|
||||||
|
street2: address.street2 ?? null,
|
||||||
|
postalCode: address.postalCode ?? "",
|
||||||
|
city: address.city ?? "",
|
||||||
|
countryCode: address.countryCode ?? "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => customerId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const customer = await getCustomer(id)
|
||||||
|
hydrateFromCustomer(customer)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = form.name.trim()
|
||||||
|
const phone = form.phone?.trim() || null
|
||||||
|
const email = form.email?.trim() || null
|
||||||
|
|
||||||
|
const customerPayload: CustomerPayload = {
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
|
if (customerId.value !== null) {
|
||||||
|
await updateCustomer(customerId.value, customerPayload)
|
||||||
|
targetId = customerId.value
|
||||||
|
} else {
|
||||||
|
const created = await createCustomer(customerPayload)
|
||||||
|
targetId = created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId !== null) {
|
||||||
|
await router.push(`/admin/customer/${targetId}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
44
frontend/pages/admin/customer/address.vue
Normal file
44
frontend/pages/admin/customer/address.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<Address type="customer" :address="address" @validate="validate"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AddressData, AddressPayload } from "~/services/address"
|
||||||
|
import { createAddress, getAddress, updateAddress } from "~/services/address"
|
||||||
|
import { getCustomer, updateCustomer } from "~/services/customer"
|
||||||
|
import type { CustomerData } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const customerId = computed(() => Number(route.query.customerId))
|
||||||
|
const customer = ref<CustomerData | null>(null)
|
||||||
|
const addressId = computed(() => (route.query.addressId !== undefined ? Number(route.query.addressId) : null))
|
||||||
|
const address = ref<AddressData | null>(null)
|
||||||
|
|
||||||
|
const validate = async (payload: AddressPayload) => {
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
await updateAddress(addressId.value, payload)
|
||||||
|
} else {
|
||||||
|
await addAddress(payload)
|
||||||
|
await router.push("/admin/customer/" + customerId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = async (payload: AddressPayload) => {
|
||||||
|
const response: AddressData = await createAddress(payload)
|
||||||
|
const addressIRI = `/api/addresses/${response.id}`
|
||||||
|
const existingIris = (customer.value?.addresses ?? [])
|
||||||
|
.map((item: any) => (typeof item === "string" ? item : `/api/addresses/${item.id}`))
|
||||||
|
.filter((iri: string | null) => Boolean(iri)) as string[]
|
||||||
|
const next = [...new Set([...existingIris, addressIRI])]
|
||||||
|
|
||||||
|
return await updateCustomer(customerId.value, { addresses: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
customer.value = await getCustomer(customerId.value)
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
address.value = await getAddress(addressId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
117
frontend/pages/admin/customer/customer-list.vue
Normal file
117
frontend/pages/admin/customer/customer-list.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des clients</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
|
<div class="max-h-96 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="sticky text-primary-700 top-0 z-10 grid grid-cols-8 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Téléphone</div>
|
||||||
|
<div>Email</div>
|
||||||
|
<div>Rue</div>
|
||||||
|
<div>Complément</div>
|
||||||
|
<div>Code Postal</div>
|
||||||
|
<div>Ville</div>
|
||||||
|
<div>Pays</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun client.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="customer in customerList" :key="customer.id">
|
||||||
|
<div
|
||||||
|
v-if="!customer.addresses || customer.addresses.length === 0"
|
||||||
|
class="grid text-primary-700 grid-cols-8 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
||||||
|
@click="goToCustomer(customer.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ customer.name || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.phone || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.email || "—" }}</div>
|
||||||
|
<div class="col-span-1">Pas d'adresse</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="customer.addresses.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(address, idx) in customer.addresses"
|
||||||
|
:key="address.id ?? `${customer.id}-${idx}-${address.street}-${address.postalCode}`"
|
||||||
|
class="grid grid-cols-8 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
:class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''"
|
||||||
|
@click="goToCustomer(customer.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">
|
||||||
|
{{ idx === 0 ? (customer.name || "—") : "↳" }}
|
||||||
|
</div>
|
||||||
|
<div class="truncate">{{ idx === 0 ? (customer.phone || "—") : "" }}</div>
|
||||||
|
<div class="truncate">{{ idx === 0 ? (customer.email || "—") : "" }}</div>
|
||||||
|
<div class="truncate">{{ address.street || "—" }}</div>
|
||||||
|
<div class="truncate">{{ address.street2 || "—" }}</div>
|
||||||
|
<div>{{ address.postalCode || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.city || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.countryCode || "—" }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-8 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
@click="goToCustomer(customer.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ customer.name || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.phone || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.email || "—" }}</div>
|
||||||
|
<div class="col-span-5 text-slate-400">
|
||||||
|
Adresses non chargées
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
|
||||||
|
Accès réservé aux administrateurs.
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/customer"
|
||||||
|
class="inline-flex items-center mb-16 justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getCustomerList } from "~/services/customer"
|
||||||
|
import type { CustomerData } from "~/services/dto/customer-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const customerList = ref<CustomerData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToCustomer = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/customer/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
customerList.value = await getCustomerList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'admin'
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>test</h1>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
203
frontend/pages/admin/supplier/[[id]].vue
Normal file
203
frontend/pages/admin/supplier/[[id]].vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
|
<Icon @click="router.push('/admin/supplier/supplier-list')" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ supplierId ? "Modification du fournisseur" : "Ajout d'un fournisseur" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
|
<UiTextInput id="supplier-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
<Icon :name="supplierId ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ supplierId ? "Valider" : "Ajouter" }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-7">
|
||||||
|
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du fournisseur</h2>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto mb-11 text-primary-700">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border bg-slate-100 border-gray-200">
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Libellé</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Rue</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Complément</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Code postal</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Ville</th>
|
||||||
|
<th class="py-3 px-4 text-sm uppercase">Pays</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-if="form.addresses.length === 0">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="py-4 text-slate-400">
|
||||||
|
Aucune adresse.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr
|
||||||
|
v-for="(address, index) in form.addresses"
|
||||||
|
:key="address.id ?? index"
|
||||||
|
class="border border-gray-100 hover:bg-slate-50"
|
||||||
|
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="goToEditAddress(address.id ?? null)"
|
||||||
|
>
|
||||||
|
<td class="py-3 px-4">{{ address.label || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.street || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.street2 || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.postalCode || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.city || "—" }}</td>
|
||||||
|
<td class="py-3 px-4">{{ address.countryCode || "—" }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center text-xl gap-2 text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
:disabled="supplierId === null || !auth.isAdmin"
|
||||||
|
@click="goToAddAddress"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
|
import {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
|
||||||
|
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
|
||||||
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
const supplierId = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const form = reactive<SupplierFormData>({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
addresses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToAddAddress = () => {
|
||||||
|
if (supplierId.value === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/supplier/address",
|
||||||
|
query: {
|
||||||
|
supplierId: String(supplierId.value),
|
||||||
|
fromSupplier: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToEditAddress = (addressId: number | null) => {
|
||||||
|
if (supplierId.value === null || addressId === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/supplier/address",
|
||||||
|
query: {
|
||||||
|
supplierId: String(supplierId.value),
|
||||||
|
addressId: String(addressId),
|
||||||
|
fromSupplier: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFromSupplier = (supplier: SupplierData | null) => {
|
||||||
|
if (!supplier) return
|
||||||
|
form.name = supplier.name ?? ""
|
||||||
|
form.email = supplier.email ?? ""
|
||||||
|
form.phone = supplier.phone ?? ""
|
||||||
|
if (!Array.isArray(supplier.addresses) || supplier.addresses.length === 0) {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof supplier.addresses[0] === "string") {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addresses = supplier.addresses.map((address) => ({
|
||||||
|
id: address.id ?? null,
|
||||||
|
label: address.label ?? "",
|
||||||
|
street: address.street ?? "",
|
||||||
|
street2: address.street2 ?? null,
|
||||||
|
postalCode: address.postalCode ?? "",
|
||||||
|
city: address.city ?? "",
|
||||||
|
countryCode: address.countryCode ?? "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => supplierId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const supplier = await getSupplier(id)
|
||||||
|
hydrateFromSupplier(supplier)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = form.name.trim()
|
||||||
|
const email = (form.email ?? "").trim() || null
|
||||||
|
const phone = (form.phone ?? "").trim() || null
|
||||||
|
|
||||||
|
const supplierPayload: SupplierPayload = {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
|
if (supplierId.value !== null) {
|
||||||
|
await updateSupplier(supplierId.value, supplierPayload)
|
||||||
|
targetId = supplierId.value
|
||||||
|
} else {
|
||||||
|
const created = await createSupplier(supplierPayload)
|
||||||
|
targetId = created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.push(`/admin/supplier/${targetId}`)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
42
frontend/pages/admin/supplier/address.vue
Normal file
42
frontend/pages/admin/supplier/address.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<Address type="supplier" :address="address" @validate="validate"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {AddressData, AddressPayload} from "~/services/address";
|
||||||
|
import {createAddress, getAddress, updateAddress} from "~/services/address";
|
||||||
|
import {getSupplier, updateSupplier} from "~/services/supplier";
|
||||||
|
import type {SupplierData} from "~/services/dto/supplier-data";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const supplierId = computed(() => { return Number(route.query.supplierId) })
|
||||||
|
const supplier = ref<SupplierData|null>(null);
|
||||||
|
const addressId = computed(() => { return route.query.addressId !== undefined ? Number(route.query.addressId) : null })
|
||||||
|
const address = ref<AddressData|null>(null)
|
||||||
|
|
||||||
|
const validate = async (address: AddressPayload) => {
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
await updateAddress(addressId.value, address)
|
||||||
|
} else {
|
||||||
|
await addAddress(address)
|
||||||
|
await router.push('/admin/supplier/' + supplierId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = async (address: AddressPayload) => {
|
||||||
|
const response: AddressData = await createAddress(address)
|
||||||
|
const addressIRI = `/api/addresses/${response.id}`
|
||||||
|
const existingIris = (supplier.value.addresses ?? []).map((item: any) => `/api/addresses/${item.id}`)
|
||||||
|
const next = [...new Set([...existingIris, addressIRI])]
|
||||||
|
|
||||||
|
return await updateSupplier(supplierId.value, { addresses: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
supplier.value = await getSupplier(supplierId.value)
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
address.value = await getAddress(addressId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
113
frontend/pages/admin/supplier/supplier-list.vue
Normal file
113
frontend/pages/admin/supplier/supplier-list.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des fournisseurs</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
|
<div class="max-h-96 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="sticky text-primary-700 top-0 z-10 grid grid-cols-7 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Mail</div>
|
||||||
|
<div>Rue</div>
|
||||||
|
<div>Complément</div>
|
||||||
|
<div>Code Postal</div>
|
||||||
|
<div>Ville</div>
|
||||||
|
<div>Pays</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="supplierList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun fournisseur.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="supplier in supplierList" :key="supplier.id">
|
||||||
|
<div
|
||||||
|
v-if="!supplier.addresses || supplier.addresses.length === 0"
|
||||||
|
class="grid text-primary-700 grid-cols-7 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ supplier.name }}</div>
|
||||||
|
<div class="truncate">{{ supplier.email }}</div>
|
||||||
|
<div class="col-span-1">Pas d'adresse</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="supplier.addresses.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(address, idx) in supplier.addresses"
|
||||||
|
:key="address.id ?? `${supplier.id}-${idx}-${address.street}-${address.postalCode}`"
|
||||||
|
class="grid grid-cols-7 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
:class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">
|
||||||
|
{{ idx === 0 ? supplier.name : "↳" }}
|
||||||
|
</div>
|
||||||
|
<div class="truncate">{{ idx === 0 ? supplier.email : "" }}</div>
|
||||||
|
<div class="truncate">{{ address.street || "—" }}</div>
|
||||||
|
<div class="truncate">{{ address.street2 || "—" }}</div>
|
||||||
|
<div>{{ address.postalCode || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.city || "—" }}</div>
|
||||||
|
<div class="uppercase truncate">{{ address.countryCode || "—" }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-7 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ supplier.name }}</div>
|
||||||
|
<div class="truncate">{{ supplier.email }}</div>
|
||||||
|
<div class="col-span-5 text-slate-400">
|
||||||
|
Adresses non chargées
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
|
||||||
|
Accès réservé aux administrateurs.
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/supplier"
|
||||||
|
class="inline-flex items-center mb-16 justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getSupplierList } from "~/services/supplier"
|
||||||
|
import type { SupplierData } from "~/services/dto/supplier-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const supplierList = ref<SupplierData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToSupplier = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/supplier/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
supplierList.value = await getSupplierList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
142
frontend/pages/admin/user/[[id]].vue
Normal file
142
frontend/pages/admin/user/[[id]].vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
|
<Icon
|
||||||
|
@click="router.push('/admin/user/list')"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="40"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
|
{{ userId ? "Modification de l'utilisateur" : "Ajout d'un utilisateur" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
|
<UiTextInput
|
||||||
|
id="user-name"
|
||||||
|
v-model="form.username"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="user-role"
|
||||||
|
v-model="form.role"
|
||||||
|
label="Role de l'utilisateur"
|
||||||
|
:options="ROLE"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiTextInput
|
||||||
|
id="user-password"
|
||||||
|
v-model="form.password"
|
||||||
|
label="Mot de passe"
|
||||||
|
type="password"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<UiButton
|
||||||
|
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || isHydrating || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
<Icon :name="userId ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ userId ? 'Valider' : 'Ajouter' }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import { ROLE } from '~/utils/constants'
|
||||||
|
import { createUser, updateUser, getUser } from '~/services/auth'
|
||||||
|
import type { UserData, UserFormData, UserPayload } from '~/services/dto/user-data'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const userId = computed(() => resolveUserId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
|
||||||
|
const resolveUserId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<UserFormData>({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
role: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hydrateFromUser = (user: UserData | null) => {
|
||||||
|
if (!user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.username = user.username ?? ''
|
||||||
|
const roles = user.roles ?? []
|
||||||
|
const hasAdmin = roles.includes('ROLE_ADMIN')
|
||||||
|
form.role = hasAdmin ? 'ROLE_ADMIN' : 'ROLE_USER'
|
||||||
|
form.password = ''
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => userId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const user = await getUser(id)
|
||||||
|
hydrateFromUser(user)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
|
||||||
|
const normalizedUsername = form.username.trim()
|
||||||
|
const normalizedRole = form.role.trim()
|
||||||
|
const normalizedPassword = form.password.trim()
|
||||||
|
|
||||||
|
const basePayload: UserPayload = {
|
||||||
|
username: normalizedUsername,
|
||||||
|
roles: normalizedRole ? [normalizedRole] : undefined,
|
||||||
|
}
|
||||||
|
if (normalizedPassword) {
|
||||||
|
basePayload.password = normalizedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId.value) {
|
||||||
|
await updateUser(userId.value, basePayload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await createUser(basePayload)
|
||||||
|
if (created) {
|
||||||
|
await router.push('/admin/user/list')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
81
frontend/pages/admin/user/list.vue
Normal file
81
frontend/pages/admin/user/list.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des utilisateurs</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
|
<div class="grid grid-cols-2 text-primary-700 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Utilisateur</div>
|
||||||
|
<div>Role</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="userList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun utilisateur.
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="user in userList"
|
||||||
|
:key="user.id"
|
||||||
|
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200 items-center"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToUser(user.id)"
|
||||||
|
@keydown.enter="goToUser(user.id)"
|
||||||
|
>
|
||||||
|
<div>{{ user.username }}</div>
|
||||||
|
<div>{{ getRoleLabels(user.roles) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-7 border border-slate-200 mb-11 px-4 py-6 text-slate-400">
|
||||||
|
Acces reserve aux administrateurs.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/user"
|
||||||
|
class="inline-flex items-center mb-16 justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { UserData } from "~/services/dto/user-data"
|
||||||
|
import { getAdminUsers } from "~/services/auth"
|
||||||
|
import { ROLE } from "~/utils/constants"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
const userList = ref<UserData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const roleLabelByValue = new Map(ROLE.map((role) => [role.value, role.label]))
|
||||||
|
|
||||||
|
const goToUser = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/user/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRoleLabels = (roles?: string[]) => {
|
||||||
|
if (!roles || roles.length === 0) {
|
||||||
|
return '---'
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles
|
||||||
|
.map((role) => roleLabelByValue.get(role) ?? role)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
userList.value = await getAdminUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,15 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap justify-center mt-8 gap-8 mb-8 md:mb-0">
|
<div class="flex flex-wrap justify-center pb-16 gap-12">
|
||||||
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
||||||
<card-link label="NOUVELLE EXPÉDITION" link="/" iconName="mdi:truck-fast-outline" />
|
<card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" />
|
||||||
<card-link label="PLAN DE SITE" link="/" iconName="mdi:warehouse" />
|
<card-link label="PLAN DE SITE" link="/infrastructure/building" iconName="material-symbols:warehouse-outline-rounded" />
|
||||||
<card-link label="RÉCEPTIONS EN ATTENTE" link="/reception/waiting-reception" iconName="mdi:truck-remove-outline" />
|
<card-link link="/reception/waiting-reception" iconName="mdi:truck-remove-outline">
|
||||||
<card-link label="EXPÉDITIONS EN ATTENTE" link="/" iconName="mdi:truck-cargo-container" />
|
<template #label>
|
||||||
<card-link label="CASES" link="/" iconName="mdi:cube-outline" />
|
Réceptions<br>EN ATTENTE
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
|
<card-link link="/shipment/waiting-shipment" iconName="mdi:truck-cargo-container">
|
||||||
|
<template #label>
|
||||||
|
EXPÉDITIONS<br>EN ATTENTE
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
|
<card-link label="CASES" link="/infrastructure/case" iconName="material-symbols:bottom-sheets-outline" />
|
||||||
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
|
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
|
||||||
<card-link label="EXPÉDITIONS FINIES" link="/" iconName="mdi:truck-delivery-outline" />
|
<card-link label="EXPÉDITIONS FINIES" link="/shipment/finish-shipment" iconName="mdi:truck-delivery-outline" />
|
||||||
<card-link label="PASSEPORT DU BOVIN" link="/" iconName="mdi:cow" />
|
<card-link link="/" iconName="mdi:cow">
|
||||||
|
<template #label>
|
||||||
|
PASSEPORT<br>DU BOVIN
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
242
frontend/pages/infrastructure/building.vue
Normal file
242
frontend/pages/infrastructure/building.vue
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<!-- En-tête de page avec retour et titre -->
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<Icon
|
||||||
|
@click="router.push('/')"
|
||||||
|
name="gg:arrow-left-o"
|
||||||
|
size="44"
|
||||||
|
class="cursor-pointer text-primary-500"
|
||||||
|
/>
|
||||||
|
<h1 class="text-3xl font-bold uppercase text-primary-500">bâtiments</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px] space-y-6">
|
||||||
|
<!-- Liste des bâtiments + rendu du plan de chaque bâtiment -->
|
||||||
|
<div
|
||||||
|
v-for="entry in buildingLayouts"
|
||||||
|
:key="entry.building.id"
|
||||||
|
>
|
||||||
|
<div class="font-semibold tracking-wide text-primary-500">
|
||||||
|
{{ entry.building.label || `Bâtiment ${entry.building.id}` }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-4">
|
||||||
|
<!-- Aucun layout disponible pour ce bâtiment -->
|
||||||
|
<div v-if="!entry.layout" class="text-sm text-slate-400">
|
||||||
|
Aucun plan de bâtiment.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grille CSS : les cases sont positionnées via spanStyle -->
|
||||||
|
<div v-else class="overflow-auto">
|
||||||
|
<div class="grid" :style="entry.gridStyle">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="cell in entry.cells"
|
||||||
|
:key="cell.key"
|
||||||
|
class="relative text-white flex h-[50px] items-center justify-center border-y-[3px] border-y-black bg-white hover:opacity-85 focus-visible:outline-none"
|
||||||
|
:class="[cell.sideBorderClass, activeLegendStatutId !== null && cell.caseStatusId !== activeLegendStatutId ? 'opacity-35 hover:opacity-70' : '']"
|
||||||
|
:style="[cell.spanStyle, cell.sideBorderStyle]"
|
||||||
|
:to="cell.caseId ? `/infrastructure/case?id=${cell.caseId}` : '/infrastructure/case'"
|
||||||
|
:title="cell.caseStatusLabel ?? undefined"
|
||||||
|
>
|
||||||
|
<!-- Le blanc latéral est géré sur ce bloc interne (conditionnel par voisinage) -->
|
||||||
|
<div
|
||||||
|
class="flex h-full w-full items-center justify-center bg-white"
|
||||||
|
:class="cell.contentInsetClass"
|
||||||
|
:style="cell.caseStyle"
|
||||||
|
>
|
||||||
|
<!-- Numéro de case -->
|
||||||
|
{{ cell.display }}
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Légende : survol d'un statut => atténue les autres cases -->
|
||||||
|
<div class="py-4">
|
||||||
|
<!-- 3 zones fixes pour forcer gauche / centre / droite sur toute la largeur -->
|
||||||
|
<div class="grid w-full grid-cols-3 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="(statut, index) in statutLegend"
|
||||||
|
:key="statut.id"
|
||||||
|
class="flex min-w-0 cursor-pointer items-center gap-2 py-1"
|
||||||
|
:class="[
|
||||||
|
index === 0 ? 'justify-self-start' : '',
|
||||||
|
index === statutLegend.length - 1 ? 'justify-self-end' : '',
|
||||||
|
index > 0 && index < statutLegend.length - 1 ? 'justify-self-center' : ''
|
||||||
|
]"
|
||||||
|
@mouseenter="activeLegendStatutId = statut.id"
|
||||||
|
@mouseleave="activeLegendStatutId = null"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="h-5 w-5 border border-slate-300"
|
||||||
|
:style="statut.couleur ? { backgroundColor: statut.couleur } : {}"
|
||||||
|
></span>
|
||||||
|
<span class="truncate text-sm uppercase text-slate-700">
|
||||||
|
{{ statut.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {BuildingData} from "~/services/dto/building-data"
|
||||||
|
import type {BuildingLayoutData} from "~/services/dto/building-layout-data"
|
||||||
|
import type {BuildingCasePositionData} from "~/services/dto/building-case-position-data"
|
||||||
|
import type {BuildingCaseStatusData} from "~/services/dto/building-case-status-data"
|
||||||
|
import {getBuildingList} from "~/services/building"
|
||||||
|
import {getStatutList} from "~/services/statut"
|
||||||
|
|
||||||
|
definePageMeta({layout: "default"})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
// Données brutes chargées depuis l'API
|
||||||
|
const buildingList = ref<BuildingData[]>([])
|
||||||
|
const statutLegend = ref<BuildingCaseStatusData[]>([])
|
||||||
|
// Statut actuellement survolé dans la légende (pour filtrage visuel)
|
||||||
|
const activeLegendStatutId = ref<number | null>(null)
|
||||||
|
// Modèle de vue prêt pour le template (layout + cellules + styles de grille)
|
||||||
|
const buildingLayouts = computed(() =>
|
||||||
|
buildingList.value.map((building) => {
|
||||||
|
// On affiche uniquement le premier layout du bâtiment
|
||||||
|
const layout = building.layouts?.[0] ?? null
|
||||||
|
const view = layout ? buildLayoutView(layout) : null
|
||||||
|
return {building, layout, cells: view?.cells ?? [], gridStyle: view?.gridStyle ?? {}}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type GridCell = {
|
||||||
|
key: string
|
||||||
|
caseId: number | null
|
||||||
|
display: string
|
||||||
|
caseStatusId: number | null
|
||||||
|
caseStatusLabel: string | null
|
||||||
|
// Couleur de fond de la case (dépend du statut)
|
||||||
|
caseStyle?: Record<string, string>
|
||||||
|
// Placement dans la grille CSS (colonne/ligne de départ + span)
|
||||||
|
spanStyle: Record<string, string>
|
||||||
|
// Bordures latérales pointillées si la case touche un gap ou le bord du plan
|
||||||
|
sideBorderClass: string
|
||||||
|
// Couleur des bordures pointillées latérales (reprend la couleur de la cellule)
|
||||||
|
sideBorderStyle?: Record<string, string>
|
||||||
|
// Espace blanc interne uniquement côté(s) adjacent(s) à une autre case
|
||||||
|
contentInsetClass: string
|
||||||
|
}
|
||||||
|
// Type intermédiaire : garde des infos utiles au calcul des bordures, retirées ensuite
|
||||||
|
type GridCellDraft = Omit<GridCell, "sideBorderClass" | "sideBorderStyle" | "contentInsetClass"> & { x: number; columnSpan: number }
|
||||||
|
|
||||||
|
// Nettoie la couleur de statut pour éviter les chaînes vides / espaces
|
||||||
|
const normalizeCaseStatusColor = (value: string | null | undefined): string | null => {
|
||||||
|
const color = (value ?? "").trim()
|
||||||
|
return color.length > 0 ? color : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles de base communs à toutes les grilles de bâtiments
|
||||||
|
const BASE_GRID_STYLE = {gridAutoRows: "1fr", rowGap: "18px", columnGap: "0px", width: "100%"} as const
|
||||||
|
|
||||||
|
// Transforme un layout API en structure de rendu (cellules + style de grille)
|
||||||
|
const buildLayoutView = (layout: BuildingLayoutData): {
|
||||||
|
cells: GridCell[];
|
||||||
|
gridStyle: Record<string, string>
|
||||||
|
} | null => {
|
||||||
|
const rows = layout.rows ?? 0, cols = layout.columns ?? 0
|
||||||
|
if (rows <= 0 || cols <= 0) return null
|
||||||
|
|
||||||
|
// Liste des positions de cases (filtre de sécurité sur les valeurs nulles)
|
||||||
|
const positions = (layout.casePositions ?? []).filter(Boolean) as BuildingCasePositionData[]
|
||||||
|
// Colonnes occupées par au moins une case (sert à détecter les gaps)
|
||||||
|
const occupiedColumns = new Set<number>()
|
||||||
|
// Sécurité : si deux positions ont le même x/y, on garde la première
|
||||||
|
const seenCoordinates = new Set<string>()
|
||||||
|
const cellDrafts: GridCellDraft[] = []
|
||||||
|
|
||||||
|
// Tri visuel : de haut en bas, puis de gauche à droite
|
||||||
|
const positionsSorted = [...positions].sort(
|
||||||
|
(leftPosition, rightPosition) =>
|
||||||
|
(leftPosition.y ?? 1) - (rightPosition.y ?? 1) || (leftPosition.x ?? 1) - (rightPosition.x ?? 1)
|
||||||
|
)
|
||||||
|
for (const position of positionsSorted) {
|
||||||
|
const x = position.x ?? 1
|
||||||
|
const y = position.y ?? 1
|
||||||
|
const coordinateKey = `${x}-${y}`
|
||||||
|
if (seenCoordinates.has(coordinateKey)) continue
|
||||||
|
seenCoordinates.add(coordinateKey)
|
||||||
|
|
||||||
|
// w/h = nombre de colonnes / lignes occupées par la case dans la grille
|
||||||
|
const columnSpan = position.w ?? 1
|
||||||
|
const rowSpan = position.h ?? 1
|
||||||
|
|
||||||
|
// Une case peut couvrir plusieurs colonnes : on les marque toutes comme occupées
|
||||||
|
for (let column = x; column < x + columnSpan; column++) {
|
||||||
|
if (column <= cols) occupiedColumns.add(column)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Métadonnées utiles au rendu / navigation / légende
|
||||||
|
const caseId = (position.buildingCase?.id ?? null) as number | null
|
||||||
|
const caseNumber = (position.buildingCase?.caseNumber ?? null) as number | null
|
||||||
|
const caseStatusId = position.buildingCase?.statut?.id ?? null
|
||||||
|
const caseStatusLabel = position.buildingCase?.statut?.label ?? null
|
||||||
|
const statusColor = normalizeCaseStatusColor(position.buildingCase?.statut?.couleur)
|
||||||
|
|
||||||
|
cellDrafts.push({
|
||||||
|
key: `case-${layout.id}-${position.id}`,
|
||||||
|
x,
|
||||||
|
columnSpan,
|
||||||
|
caseId,
|
||||||
|
display: caseNumber !== null ? String(caseNumber) : "Case",
|
||||||
|
caseStatusId,
|
||||||
|
caseStatusLabel,
|
||||||
|
caseStyle: statusColor ? {backgroundColor: statusColor} : undefined,
|
||||||
|
// Exemple : "14 / span 1" => commence en colonne 14 et occupe 1 colonne
|
||||||
|
spanStyle: {gridColumn: `${x} / span ${columnSpan}`, gridRow: `${y} / span ${rowSpan}`}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colonnes vides = gaps visuels (plus étroites dans la grille)
|
||||||
|
const gapColumns = Array.from({length: cols}, (_, i) => i + 1).filter((x) => !occupiedColumns.has(x))
|
||||||
|
const gapSet = new Set(gapColumns)
|
||||||
|
|
||||||
|
// Ajoute les bordures latérales pointillées pour les cases au contact d'un gap ou d'un bord
|
||||||
|
const cells: GridCell[] = cellDrafts.map(({x, columnSpan, ...cell}) => {
|
||||||
|
const touchesLeftGapOrEdge = x === 1 || gapSet.has(x - 1)
|
||||||
|
const touchesRightGapOrEdge = x + columnSpan - 1 === cols || gapSet.has(x + columnSpan)
|
||||||
|
const sideBorderClass = [
|
||||||
|
touchesLeftGapOrEdge ? "border-l-[3px] [border-left-style:dashed]" : "",
|
||||||
|
touchesRightGapOrEdge ? "border-r-[3px] [border-right-style:dashed]" : ""
|
||||||
|
].filter(Boolean).join(" ")
|
||||||
|
// Les pointillés latéraux reprennent la couleur de la cellule (si un statut en fournit une)
|
||||||
|
const sideBorderStyle = {
|
||||||
|
...(cell.caseStyle?.backgroundColor && touchesLeftGapOrEdge ? {borderLeftColor: cell.caseStyle.backgroundColor} : {}),
|
||||||
|
...(cell.caseStyle?.backgroundColor && touchesRightGapOrEdge ? {borderRightColor: cell.caseStyle.backgroundColor} : {})
|
||||||
|
}
|
||||||
|
// Le "blanc" n'est ajouté qu'entre deux cellules adjacentes (pas sur bord/gap)
|
||||||
|
const contentInsetClass = [
|
||||||
|
!touchesLeftGapOrEdge ? "ml-[4px]" : "",
|
||||||
|
!touchesRightGapOrEdge ? "mr-[4px]" : ""
|
||||||
|
].filter(Boolean).join(" ")
|
||||||
|
return {...cell, sideBorderClass, sideBorderStyle, contentInsetClass}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Les colonnes de gap sont rendues en 24px, les autres occupent l'espace restant
|
||||||
|
const columnsTemplate = Array.from({length: cols}, (_, i) => (gapSet.has(i + 1) ? "24px" : "minmax(0, 1fr)")).join(" ")
|
||||||
|
return {cells, gridStyle: {gridTemplateColumns: columnsTemplate, ...BASE_GRID_STYLE}}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Chargement initial des bâtiments et de la légende des statuts
|
||||||
|
const buildings = await getBuildingList()
|
||||||
|
const statuts = await getStatutList()
|
||||||
|
buildingList.value = buildings
|
||||||
|
// Tri alphabétique FR pour une légende stable
|
||||||
|
statutLegend.value = [...statuts].sort((a, b) =>
|
||||||
|
(a.label ?? "").localeCompare(b.label ?? "", "fr", {sensitivity: "base"})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
27
frontend/pages/infrastructure/case.vue
Normal file
27
frontend/pages/infrastructure/case.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center items-center">
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="!hasCaseId"
|
||||||
|
@click="printCaseReport"
|
||||||
|
>
|
||||||
|
Imprimer
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const caseId = computed(() => Number(route.query.id))
|
||||||
|
const hasCaseId = computed(() => Number.isFinite(caseId.value) && caseId.value > 0)
|
||||||
|
|
||||||
|
const printCaseReport = async () => {
|
||||||
|
if (!hasCaseId.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `tableau_poids_case_${caseId.value}.pdf`
|
||||||
|
await printPdf(`/building_cases/${caseId.value}/weights-report`, filename)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<select
|
<select
|
||||||
id="user-select"
|
id="user-select"
|
||||||
v-model="selectedUsername"
|
v-model="selectedUsername"
|
||||||
|
autocomplete="username"
|
||||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200"
|
||||||
:disabled="isLoadingUsers"
|
:disabled="isLoadingUsers"
|
||||||
>
|
>
|
||||||
@@ -39,13 +40,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</button>
|
</UiButton>
|
||||||
<p class="font-bold">v{{ version }}</p>
|
<p class="font-bold">v{{ version }}</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex justify-between h-[52px] mb-[80px]">
|
||||||
<div class="flex justify-between h-[52px] mb-[80px]">
|
<div class="flex flex-1 mr-16">
|
||||||
<div class="flex flex-1 mr-16">
|
<UiStepper
|
||||||
<UiStepper
|
:labels="RECEPTION_STEP_LABELS"
|
||||||
:labels="RECEPTION_STEP_LABELS"
|
:current-step="storeReception?.currentStep ?? 0"
|
||||||
:current-step="storeReception?.currentStep ?? 0"
|
@select="handleStepSelect"
|
||||||
@select="handleStepSelect"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<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 || storeReception.currentStep === 0"/>
|
<UiButton
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
type="button"
|
||||||
<ReceptionProductReceived
|
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
||||||
v-if="storeReception?.currentStep === 2 &&
|
@click="saveAndHold"
|
||||||
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
|
>Mettre en attente
|
||||||
<ReceptionBovineReceived
|
</UiButton>
|
||||||
v-if="storeReception?.currentStep === 2 &&
|
|
||||||
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||||
|
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
||||||
|
<ReceptionProductReceived
|
||||||
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
|
||||||
|
<ReceptionBovineReceived
|
||||||
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
||||||
|
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-start gap-10">
|
<div class="flex items-center justify-start gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" />
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase">listes des réceptions finie</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions finie</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ps-20 " >
|
<div class="px-[86px]">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
<div>Numéro</div>
|
<div>Numéro</div>
|
||||||
@@ -20,13 +20,14 @@
|
|||||||
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@click="goToReception(reception.id)"
|
||||||
>
|
>
|
||||||
<div>{{ reception.identificationNumber}}</div>
|
<div>{{ reception.identificationNumber}}</div>
|
||||||
<div>{{ reception.receptionDate}}</div>
|
<div>{{ reception.receptionDate}}</div>
|
||||||
<div>{{ reception.supplier?.name }}</div>
|
<div>{{ reception.supplier?.name }}</div>
|
||||||
<div>{{ reception.address?.fullAddress }}</div>
|
<div>{{ reception.address?.fullAddress }}</div>
|
||||||
<div>{{ reception.receptionType?.label }}</div>
|
<div>{{ reception.receptionType?.label }}</div>
|
||||||
<div>{{ formatWeighing(reception, 'gross') }} | {{ formatWeighing(reception, 'tare') }}</div>
|
<div>{{ formatWeighing(reception) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,16 +36,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {ReceptionData} from "~/services/dto/reception-data";
|
import type {ReceptionData} from "~/services/dto/reception-data";
|
||||||
import {getReceptionList} from "~/services/reception";
|
import {getReceptionList} from "~/services/reception";
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
|
||||||
const receptionList = ref<ReceptionData[]>()
|
const receptionList = ref<ReceptionData[]>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const formatWeighing = (reception: ReceptionData, type: 'gross' | 'tare') => {
|
const formatWeighing = (reception: ReceptionData) => {
|
||||||
const entry = reception.weights?.find((weight) => weight.type === type)
|
const gross = reception.weights?.find((weight) => weight.type === 'gross')?.weight
|
||||||
if (!entry || entry.weight == null || entry.dsd == null) {
|
const tare = reception.weights?.find((weight) => weight.type === 'tare')?.weight
|
||||||
|
|
||||||
|
if (gross == null || tare == null) {
|
||||||
return '—'
|
return '—'
|
||||||
}
|
}
|
||||||
return `${entry.weight} kg`
|
|
||||||
|
return `${gross - tare} kg`
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToReception = (id: number) => {
|
||||||
|
router.push(`/reception/update/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
999
frontend/pages/reception/update/[[id]].vue
Normal file
999
frontend/pages/reception/update/[[id]].vue
Normal file
@@ -0,0 +1,999 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-[60px]">
|
||||||
|
<div class="flex items-center justify-between gap-10 relative">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] justify-between">
|
||||||
|
<Icon @click="router.push('/reception/finish-reception')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Réception {{ form.identificationNumber }}</h1>
|
||||||
|
<Icon @click="printReceipt" name="mdi:printer-outline" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<!-- Nom de l'utilisateur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-user"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
v-model="form.userId"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:options="users.map((user) => ({
|
||||||
|
value: String(user.id),
|
||||||
|
label: user.username
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingUsers"
|
||||||
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
/>
|
||||||
|
<!-- Date de réception -->
|
||||||
|
<UiDateInput
|
||||||
|
id="reception-date"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
v-model="form.receptionDate"
|
||||||
|
label="Date de réception"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- type de reception -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-supplier"
|
||||||
|
v-model="form.receptionTypeId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label="Type de Réception"
|
||||||
|
:options="receptionTypes.map((receptionType) => ({
|
||||||
|
value: String(receptionType.id),
|
||||||
|
label: receptionType.label
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingSuppliers"
|
||||||
|
wrapper-class="col-start-1 row-start-4"
|
||||||
|
/>
|
||||||
|
<!-- Fournisseur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-supplier"
|
||||||
|
v-model="form.supplierId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label="Fournisseur"
|
||||||
|
:options="suppliers.map((supplier) => ({
|
||||||
|
value: String(supplier.id),
|
||||||
|
label: supplier.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingSuppliers"
|
||||||
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
/>
|
||||||
|
<!-- Adresse fournisseur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
label="Adresse"
|
||||||
|
:options="supplierAddresses.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))"
|
||||||
|
:disabled="(isLoadingSuppliers || supplierAddresses.length === 0) && !auth.isAdmin"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
<!-- Camion -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-truck"
|
||||||
|
v-model="form.truckId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label="Camion"
|
||||||
|
:options="trucks.map((truck) => ({
|
||||||
|
value: String(truck.id),
|
||||||
|
label: truck.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingTrucks"
|
||||||
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
/>
|
||||||
|
<!-- Transporteur -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-carrier"
|
||||||
|
v-model="form.carrierId"
|
||||||
|
label="Transporteur"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
:options="carriers.map((carrier) => ({
|
||||||
|
value: String(carrier.id),
|
||||||
|
label: carrier.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingCarriers"
|
||||||
|
select-class="h-[34px]"
|
||||||
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
wrapper-class="col-start-2 row-start-5"
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
/>
|
||||||
|
<!-- Plaque d'immatriculation -->
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Immatriculation (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
id="reception-vehicle"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="(isLoadingVehicles || filteredVehicles.length === 0) && !auth.isAdmin"
|
||||||
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="formIsLoading">
|
||||||
|
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer "
|
||||||
|
:class="activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weights'"
|
||||||
|
>
|
||||||
|
pesée à plein
|
||||||
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer "
|
||||||
|
:class="activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500 ' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weightsEmpty'"
|
||||||
|
>
|
||||||
|
pesée à vide
|
||||||
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer "
|
||||||
|
:class="activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'merchandise'"
|
||||||
|
>
|
||||||
|
{{ isMerchandise ? "Marchandise" : "Bovins" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mb-12 ">
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weights'"
|
||||||
|
v-model="grossWeight"
|
||||||
|
v-if="grossWeight"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weightsEmpty'"
|
||||||
|
v-model="tareWeight"
|
||||||
|
v-if="tareWeight"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<update-merchandise
|
||||||
|
v-if="activeTab === 'merchandise' && isMerchandise"
|
||||||
|
v-model="merchandiseForm"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<update-bovin
|
||||||
|
v-if="activeTab === 'merchandise' && !isMerchandise"
|
||||||
|
v-model="bovineEntries"
|
||||||
|
v-model:otherQuantity="bovineOtherQuantity"
|
||||||
|
:isAdmin="auth.isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex mb-16 items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2 justify-self-end"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePdfPrinter } from '#imports'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import UpdateBovin from '~/components/reception/update-bovin.vue'
|
||||||
|
import UpdateMerchandise from '~/components/reception/update-merchandise.vue'
|
||||||
|
import UpdateWeight from '~/components/commun/update-weight.vue'
|
||||||
|
import { getUsers } from '~/services/auth'
|
||||||
|
import { getCarrierList } from '~/services/carrier'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
|
||||||
|
import type {
|
||||||
|
MerchandiseEntryData,
|
||||||
|
ReceptionData,
|
||||||
|
ReceptionFormData,
|
||||||
|
} from '~/services/dto/reception-data'
|
||||||
|
import type { WeightEntryData } from '~/services/dto/weight-data'
|
||||||
|
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||||
|
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||||
|
import type { TruckData } from '~/services/dto/truck-data'
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
import { getDriverList } from '~/services/driver'
|
||||||
|
import {
|
||||||
|
createReceptionBovine,
|
||||||
|
deleteReceptionBovine,
|
||||||
|
getReceptionBovineList,
|
||||||
|
updateReceptionBovine
|
||||||
|
} from '~/services/reception-bovine'
|
||||||
|
import {
|
||||||
|
createReceptionPelletBuilding,
|
||||||
|
deleteReceptionPelletBuilding,
|
||||||
|
getReceptionPelletBuildingList
|
||||||
|
} from '~/services/reception-pellet-building'
|
||||||
|
import { getReception, updateReception } from '~/services/reception'
|
||||||
|
import { getReceptionTypeList } from '~/services/reception-type'
|
||||||
|
import { getSupplierList } from '~/services/supplier'
|
||||||
|
import { getTruckList } from '~/services/truck'
|
||||||
|
import { getVehicleList } from '~/services/vehicle'
|
||||||
|
import { createWeight, updateWeight } from '~/services/weight'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
|
import { RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const receptionStore = useReceptionStore()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const activeTab = ref<'weightsEmpty' | 'weights' | 'merchandise'>('weights')
|
||||||
|
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
||||||
|
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
||||||
|
const bovineEntries = ref<ReceptionBovineTypeData[]>([])
|
||||||
|
const bovineOtherQuantity = ref<number | null>(0)
|
||||||
|
const merchandiseForm = ref<MerchandiseEntryData>({
|
||||||
|
merchandiseTypeId: '',
|
||||||
|
merchandiseDetail: '',
|
||||||
|
selectedBuildingIds: [],
|
||||||
|
selectedPelletBuildingIds: {}
|
||||||
|
})
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingTypes = ref(false)
|
||||||
|
const suppliers = ref<SupplierData[]>([])
|
||||||
|
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||||
|
const isLoadingSuppliers = ref(false)
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const formIsLoading = ref(false)
|
||||||
|
const isMerchandise = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const vehicleSyncLock = ref(false)
|
||||||
|
|
||||||
|
const idReception = Number(route.params.id)
|
||||||
|
|
||||||
|
function runWithVehicleSyncLock(mutator: () => void) {
|
||||||
|
if (vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vehicleSyncLock.value = true
|
||||||
|
try {
|
||||||
|
mutator()
|
||||||
|
} finally {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
vehicleSyncLock.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<ReceptionFormData>({
|
||||||
|
identificationNumber: null,
|
||||||
|
licensePlate: '',
|
||||||
|
receptionDate: new Date().toISOString().slice(0, 10),
|
||||||
|
receptionTypeId: '',
|
||||||
|
userId: '',
|
||||||
|
supplierId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
const supplierAddresses = computed(() => {
|
||||||
|
const supplierId = Number(form.supplierId)
|
||||||
|
if (!Number.isFinite(supplierId)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? []
|
||||||
|
})
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return vehicles.value.filter(
|
||||||
|
(vehicle) =>
|
||||||
|
String(vehicle.carrier?.id) === form.carrierId &&
|
||||||
|
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => idReception,
|
||||||
|
async (id) => {
|
||||||
|
if (!Number.isFinite(id) || id <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const reception = await getReception(id)
|
||||||
|
hydrateFromReception(reception)
|
||||||
|
await loadBovineEntries(id)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.receptionTypeId,
|
||||||
|
() => {
|
||||||
|
const receptionType = receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||||
|
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function createEmptyWeightEntry(type: 'gross' | 'tare'): WeightEntryData {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
dsd: null,
|
||||||
|
weight: null,
|
||||||
|
weighedAt: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearReceptionBovines(receptionId: number) {
|
||||||
|
const receptionIri = `/api/receptions/${receptionId}`
|
||||||
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
|
for (const selection of existing) {
|
||||||
|
await deleteReceptionBovine(selection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBovineEntries(receptionId: number) {
|
||||||
|
const receptionIri = `/api/receptions/${receptionId}`
|
||||||
|
bovineEntries.value = await getReceptionBovineList(receptionIri)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncMerchandiseFlag() {
|
||||||
|
const receptionType =
|
||||||
|
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||||
|
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRelationId(value: unknown): string | null {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const match = value.match(/\/(\d+)$/)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && 'id' in value) {
|
||||||
|
const relation = value as { id?: number | string }
|
||||||
|
if (typeof relation.id === 'number') {
|
||||||
|
return String(relation.id)
|
||||||
|
}
|
||||||
|
if (typeof relation.id === 'string') {
|
||||||
|
return relation.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearReceptionMerchandise(receptionId: number) {
|
||||||
|
const receptionIri = `/api/receptions/${receptionId}`
|
||||||
|
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||||
|
for (const selection of existing) {
|
||||||
|
await deleteReceptionPelletBuilding(selection.id)
|
||||||
|
}
|
||||||
|
await updateReception(receptionId, {
|
||||||
|
merchandiseType: null,
|
||||||
|
merchandiseDetail: null,
|
||||||
|
buildings: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateFromReception(reception: ReceptionData | null) {
|
||||||
|
if (!reception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.identificationNumber = reception?.identificationNumber ?? ''
|
||||||
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
|
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
|
||||||
|
form.userId = reception?.user?.id
|
||||||
|
? String(reception.user.id)
|
||||||
|
: form.userId
|
||||||
|
form.supplierId = reception?.supplier?.id
|
||||||
|
? String(reception.supplier.id)
|
||||||
|
: ''
|
||||||
|
form.addressId = reception?.address?.id
|
||||||
|
? String(reception.address.id)
|
||||||
|
: ''
|
||||||
|
form.truckId = reception?.truck?.id
|
||||||
|
? String(reception.truck.id)
|
||||||
|
: ''
|
||||||
|
form.carrierId = reception?.carrier?.id
|
||||||
|
? String(reception.carrier.id)
|
||||||
|
: ''
|
||||||
|
form.driverId = reception?.driver?.id
|
||||||
|
? String(reception.driver.id)
|
||||||
|
: ''
|
||||||
|
form.receptionTypeId = reception?.receptionType?.id
|
||||||
|
? String(reception.receptionType.id)
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const selectionMap: Record<string, string[]> = {}
|
||||||
|
for (const selection of reception?.pelletBuildings ?? []) {
|
||||||
|
const pelletTypeId = getRelationId(selection.pelletType)
|
||||||
|
const buildingId = getRelationId(selection.building)
|
||||||
|
if (!pelletTypeId || !buildingId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!selectionMap[pelletTypeId]) {
|
||||||
|
selectionMap[pelletTypeId] = []
|
||||||
|
}
|
||||||
|
selectionMap[pelletTypeId].push(buildingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
merchandiseForm.value = {
|
||||||
|
merchandiseTypeId: reception?.merchandiseType?.id ? String(reception.merchandiseType.id) : '',
|
||||||
|
merchandiseDetail: reception?.merchandiseDetail ?? '',
|
||||||
|
selectedBuildingIds: reception?.buildings?.map((building) => String(building.id)) ?? [],
|
||||||
|
selectedPelletBuildingIds: selectionMap
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedOther =
|
||||||
|
typeof reception?.bovineDetail === 'string' && reception.bovineDetail.trim() !== ''
|
||||||
|
? Number(reception.bovineDetail)
|
||||||
|
: 0
|
||||||
|
bovineOtherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
|
||||||
|
const gross = reception.weights?.find((weight) => weight.type === 'gross') ?? null
|
||||||
|
const tare = reception.weights?.find((weight) => weight.type === 'tare') ?? null
|
||||||
|
grossWeight.value = gross ? { ...gross } : createEmptyWeightEntry('gross')
|
||||||
|
tareWeight.value = tare ? { ...tare } : createEmptyWeightEntry('tare')
|
||||||
|
isHydrating.value = false
|
||||||
|
syncMerchandiseFlag()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSuppliers() {
|
||||||
|
isLoadingSuppliers.value = true
|
||||||
|
try {
|
||||||
|
suppliers.value = await getSupplierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingSuppliers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTypes() {
|
||||||
|
isLoadingTypes.value = true
|
||||||
|
try {
|
||||||
|
receptionTypes.value = await getReceptionTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTypes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTrucks() {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCarriers() {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDrivers() {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadVehicles() {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultUser() {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printReceipt() {
|
||||||
|
if (!import.meta.client || !Number.isFinite(idReception) || idReception <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierName =
|
||||||
|
suppliers.value.find((supplier) => String(supplier.id) === form.supplierId)?.name ??
|
||||||
|
'fournisseur'
|
||||||
|
const filename = `${form.identificationNumber || idReception}_${supplierName}_${form.licensePlate || 'immat'}.pdf`
|
||||||
|
await printPdf(`/receptions/${idReception}/receipt`, filename)
|
||||||
|
|
||||||
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveWeightEntry(entry: WeightEntryData) {
|
||||||
|
if (!idReception || entry.weight === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: entry.type,
|
||||||
|
dsd: entry.dsd ?? null,
|
||||||
|
weight: entry.weight,
|
||||||
|
weighedAt: entry.weighedAt ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.id) {
|
||||||
|
await updateWeight(entry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if id is missing in local state, reuse existing weight by type.
|
||||||
|
const reception = await getReception(idReception)
|
||||||
|
const existingEntry = reception?.weights?.find((weight) => weight.type === entry.type) ?? null
|
||||||
|
if (existingEntry?.id) {
|
||||||
|
await updateWeight(existingEntry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWeight({
|
||||||
|
reception: `/api/receptions/${idReception}`,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function saveBovineEntry(entry: ReceptionBovineTypeData) {
|
||||||
|
if (!idReception || !entry.bovineType || entry.quantity === null || entry.quantity <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
quantity: entry.quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.id) {
|
||||||
|
await updateReceptionBovine(entry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createReceptionBovine({
|
||||||
|
reception: `/api/receptions/${idReception}`,
|
||||||
|
bovineType: `/api/bovine_types/${entry.bovineType.id}`,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTotalBovines() {
|
||||||
|
const totalTypes = bovineEntries.value.reduce(
|
||||||
|
(sum, entry) => sum + (entry.quantity ?? 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
return totalTypes +
|
||||||
|
(bovineOtherQuantity.value ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncBovineEntries() {
|
||||||
|
if (!idReception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const receptionIri = `/api/receptions/${idReception}`
|
||||||
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
|
const currentPositive = bovineEntries.value.filter(
|
||||||
|
(entry) => (entry.quantity ?? 0) > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const existingEntry of existing) {
|
||||||
|
const stillPresent = currentPositive.some(
|
||||||
|
(entry) => entry.bovineType.id === existingEntry.bovineType.id
|
||||||
|
)
|
||||||
|
if (!stillPresent) {
|
||||||
|
await deleteReceptionBovine(existingEntry.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of currentPositive) {
|
||||||
|
await saveBovineEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveMerchandiseEntry(
|
||||||
|
receptionIri: string,
|
||||||
|
pelletTypeId: string,
|
||||||
|
buildingId: string,
|
||||||
|
existingKeys: Set<string>
|
||||||
|
) {
|
||||||
|
const key = `${pelletTypeId}:${buildingId}`
|
||||||
|
if (existingKeys.has(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await createReceptionPelletBuilding({
|
||||||
|
reception: receptionIri,
|
||||||
|
pelletType: `/api/pellet_types/${pelletTypeId}`,
|
||||||
|
building: `/api/buildings/${buildingId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncMerchandiseEntries() {
|
||||||
|
if (!idReception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const receptionIri = `/api/receptions/${idReception}`
|
||||||
|
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||||
|
const existingMap = new Map<string, number>()
|
||||||
|
for (const selection of existing) {
|
||||||
|
const pelletTypeId = getRelationId(selection.pelletType)
|
||||||
|
const buildingId = getRelationId(selection.building)
|
||||||
|
if (!pelletTypeId || !buildingId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingMap.set(`${pelletTypeId}:${buildingId}`, selection.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
|
||||||
|
for (const [pelletTypeId, buildingIds] of Object.entries(
|
||||||
|
merchandiseForm.value.selectedPelletBuildingIds
|
||||||
|
)) {
|
||||||
|
for (const buildingId of buildingIds) {
|
||||||
|
desiredEntries.push({ pelletTypeId, buildingId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const desiredKeys = new Set(
|
||||||
|
desiredEntries.map((entry) => `${entry.pelletTypeId}:${entry.buildingId}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [key, selectionId] of existingMap.entries()) {
|
||||||
|
if (!desiredKeys.has(key)) {
|
||||||
|
await deleteReceptionPelletBuilding(selectionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingKeys = new Set(existingMap.keys())
|
||||||
|
for (const entry of desiredEntries) {
|
||||||
|
await saveMerchandiseEntry(
|
||||||
|
receptionIri,
|
||||||
|
entry.pelletTypeId,
|
||||||
|
entry.buildingId,
|
||||||
|
existingKeys
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
|
const normalizedReceptionDate = form.receptionDate.trim()
|
||||||
|
const normalizedReceptionTypeId = form.receptionTypeId.trim()
|
||||||
|
const normalizedUserId = form.userId.trim()
|
||||||
|
const normalizedSupplierId = form.supplierId.trim()
|
||||||
|
const normalizedAddressId = form.addressId.trim()
|
||||||
|
const normalizedTruckId = form.truckId.trim()
|
||||||
|
const normalizedCarrierId = form.carrierId.trim()
|
||||||
|
const normalizedDriverId = form.driverId.trim()
|
||||||
|
const userIri = normalizedUserId
|
||||||
|
? `/api/users/${normalizedUserId}`
|
||||||
|
: null
|
||||||
|
const supplierIri = normalizedSupplierId
|
||||||
|
? `/api/suppliers/${normalizedSupplierId}`
|
||||||
|
: null
|
||||||
|
const addressIri = normalizedAddressId
|
||||||
|
? `/api/addresses/${normalizedAddressId}`
|
||||||
|
: null
|
||||||
|
const truckIri = normalizedTruckId
|
||||||
|
? `/api/trucks/${normalizedTruckId}`
|
||||||
|
: null
|
||||||
|
const carrierIri = normalizedCarrierId
|
||||||
|
? `/api/carriers/${normalizedCarrierId}`
|
||||||
|
: null
|
||||||
|
const driverIri = normalizedDriverId
|
||||||
|
? `/api/drivers/${normalizedDriverId}`
|
||||||
|
: null
|
||||||
|
const typeIri = normalizedReceptionTypeId
|
||||||
|
? `/api/reception_types/${normalizedReceptionTypeId}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
const basePayload = {
|
||||||
|
licensePlate: normalizedLicensePlate,
|
||||||
|
receptionDate: normalizedReceptionDate,
|
||||||
|
receptionType: typeIri,
|
||||||
|
user: userIri,
|
||||||
|
supplier: supplierIri,
|
||||||
|
address: addressIri,
|
||||||
|
truck: truckIri,
|
||||||
|
carrier: carrierIri
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...basePayload,
|
||||||
|
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idReception) {
|
||||||
|
await receptionStore.updateReception(idReception, {
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
await saveWeightEntry(grossWeight.value)
|
||||||
|
await saveWeightEntry(tareWeight.value)
|
||||||
|
|
||||||
|
if (isMerchandise.value) {
|
||||||
|
await clearReceptionBovines(idReception)
|
||||||
|
await updateReception(idReception, {
|
||||||
|
merchandiseType: merchandiseForm.value.merchandiseTypeId
|
||||||
|
? `/api/merchandise_types/${merchandiseForm.value.merchandiseTypeId}`
|
||||||
|
: null,
|
||||||
|
merchandiseDetail: merchandiseForm.value.merchandiseDetail.trim() || null,
|
||||||
|
buildings: merchandiseForm.value.selectedBuildingIds.map(
|
||||||
|
(buildingId) => `/api/buildings/${buildingId}`
|
||||||
|
),
|
||||||
|
bovineDetail: null,
|
||||||
|
bovinesTypes: null
|
||||||
|
})
|
||||||
|
await syncMerchandiseEntries()
|
||||||
|
} else {
|
||||||
|
if (getTotalBovines() > 52) {
|
||||||
|
// toast/erreur UI
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await clearReceptionMerchandise(idReception)
|
||||||
|
await syncBovineEntries()
|
||||||
|
await updateReception(idReception, {
|
||||||
|
bovineDetail: bovineOtherQuantity.value ? String(bovineOtherQuantity.value) : null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Évite une réhydratation complète après save (source de cascades de watchers).
|
||||||
|
// On recharge uniquement les bovins quand on est en mode bovins.
|
||||||
|
if (!isMerchandise.value) {
|
||||||
|
await loadBovineEntries(idReception)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadTypes()
|
||||||
|
syncMerchandiseFlag()
|
||||||
|
formIsLoading.value = true
|
||||||
|
await loadUsers()
|
||||||
|
await loadSuppliers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadDrivers()
|
||||||
|
await loadVehicles()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.supplierId, form.addressId, suppliers.value],
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.supplierId) {
|
||||||
|
if (form.addressId !== '') {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && supplierAddresses.value.length === 1) {
|
||||||
|
const nextAddressId = String(supplierAddresses.value[0].id)
|
||||||
|
if (form.addressId !== nextAddressId) {
|
||||||
|
form.addressId = nextAddressId
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = supplierAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (supplierAddresses.value.length === 1) {
|
||||||
|
const nextAddressId = String(supplierAddresses.value[0].id)
|
||||||
|
if (form.addressId !== nextAddressId) {
|
||||||
|
form.addressId = nextAddressId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (form.addressId !== '') {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value || vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.carrierId && idReception == null) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value && idReception == null) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredDrivers.value.length === 1) {
|
||||||
|
const nextDriverId = String(filteredDrivers.value[0].id)
|
||||||
|
if (form.driverId !== nextDriverId) {
|
||||||
|
form.driverId = nextDriverId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
const nextVehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
if (form.vehicleId !== nextVehicleId) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = nextVehicleId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.truckId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value || vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
const nextVehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
if (form.vehicleId !== nextVehicleId) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = nextVehicleId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = filteredVehicles.value.some(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (form.vehicleId !== '') {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selected = filteredVehicles.value.find(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (selected && form.licensePlate !== selected.plate) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.licensePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (vehicleSyncLock.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value || form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const match = filteredVehicles.value.find(
|
||||||
|
(vehicle) => vehicle.plate === form.licensePlate
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
const nextVehicleId = String(match.id)
|
||||||
|
if (form.vehicleId !== nextVehicleId) {
|
||||||
|
runWithVehicleSyncLock(() => {
|
||||||
|
form.vehicleId = nextVehicleId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between ">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-10">
|
<div class="flex items-center gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" />
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase">listes des réceptions en attente</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions en attente</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ps-20 " >
|
<div class="px-[86px]">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
<div class="mt-6 border border-slate-200 mb-16">
|
||||||
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
<div>Fournisseur</div>
|
<div>Fournisseur</div>
|
||||||
<div>Adresse</div>
|
<div>Adresse</div>
|
||||||
|
|||||||
83
frontend/pages/shipment/[[id]].vue
Normal file
83
frontend/pages/shipment/[[id]].vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between h-[52px] mb-[80px]">
|
||||||
|
<div class="flex flex-1 mr-16">
|
||||||
|
<UiStepper
|
||||||
|
:labels="SHIPMENT_STEP_LABELS"
|
||||||
|
:current-step="storeShipment?.currentStep ?? 0"
|
||||||
|
@select="handleStepSelect"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UiButton
|
||||||
|
type="button"
|
||||||
|
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
||||||
|
@click="saveAndHold"
|
||||||
|
>Mettre en attente
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
||||||
|
<ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
|
||||||
|
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
||||||
|
<ShipmentWeight v-if="storeShipment?.currentStep === 3" mode="tare"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {SHIPMENT_STEP_LABELS} from "~/constants/steps";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useShipmentStore} from "~/stores/shipment";
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const {current: storeShipment} = storeToRefs(shipmentStore)
|
||||||
|
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const resolveShipmentId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch (
|
||||||
|
() => route.params.id,
|
||||||
|
async (param) => {
|
||||||
|
const id = resolveShipmentId(param)
|
||||||
|
if (id === null) {
|
||||||
|
shipmentStore.clearCurrent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await shipmentStore.loadShipment(id)
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveAndHold = async () => {
|
||||||
|
if (shipmentFormRef.value) {
|
||||||
|
await shipmentFormRef.value.saveDraft()
|
||||||
|
}
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
const handleStepSelect = async (step: number) => {
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === shipmentStore.current.currentStep) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: step
|
||||||
|
})
|
||||||
|
await shipmentStore.loadShipment(shipmentStore.current.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
85
frontend/pages/shipment/finish-shipment.vue
Normal file
85
frontend/pages/shipment/finish-shipment.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-start gap-10">
|
||||||
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions finie</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px]">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
|
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Numéro</div>
|
||||||
|
<div>Date</div>
|
||||||
|
<div>Client</div>
|
||||||
|
<div>Adresse</div>
|
||||||
|
<div>Type d'expéditon</div>
|
||||||
|
<div>Poids</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="shipment in shipmentList"
|
||||||
|
:key="shipment
|
||||||
|
.id"
|
||||||
|
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goShipment(shipment.id)"
|
||||||
|
>
|
||||||
|
<div>{{ shipment.identificationNumber }}</div>
|
||||||
|
<div>{{ shipment.shipmentDate }}</div>
|
||||||
|
<div>{{ shipment.customer?.name }}</div>
|
||||||
|
<div>{{ shipment.address?.fullAddress }}</div>
|
||||||
|
<div>
|
||||||
|
<template v-if="formatShipmentLines(shipment).length">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in formatShipmentLines(shipment)"
|
||||||
|
:key="index"
|
||||||
|
class="leading-5"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>{{ formatWeighing(shipment) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
import {getShipmentList} from "~/services/shipment";
|
||||||
|
|
||||||
|
const shipmentList = ref<ShipmentData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formatWeighing = (shipment: ShipmentData) => {
|
||||||
|
const gross = shipment.weights?.find((weight) => weight.type === 'gross')?.weight
|
||||||
|
const tare = shipment.weights?.find((weight) => weight.type === 'tare')?.weight
|
||||||
|
|
||||||
|
if (gross == null || tare == null) {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${gross - tare} kg`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const formatShipmentLines = (shipment: ShipmentData) => {
|
||||||
|
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = typeof shipment.shipmentType === 'string'
|
||||||
|
? shipment.shipmentType
|
||||||
|
: shipment.shipmentType?.label
|
||||||
|
|
||||||
|
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
const goShipment = (id: number) => {
|
||||||
|
router.push(`/shipment/update/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
shipmentList.value = await getShipmentList(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
639
frontend/pages/shipment/update/[[id]].vue
Normal file
639
frontend/pages/shipment/update/[[id]].vue
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
||||||
|
<div class="flex items-center justify-between gap-10 relative col-start-1 row-start-1">
|
||||||
|
<div class="flex flex-row absolute -left-[60px] justify-between">
|
||||||
|
<Icon @click="router.push('/shipment/finish-shipment')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Expédition {{ form.identificationNumber }}</h1>
|
||||||
|
<Icon @click="printReceipt" name="mdi:printer-outline" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-user"
|
||||||
|
v-model="form.userId"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:options="users.map((user) => ({
|
||||||
|
value: String(user.id),
|
||||||
|
label: user.username
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingUsers"
|
||||||
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiDateInput
|
||||||
|
id="shipment-date"
|
||||||
|
v-model="form.shipmentDate"
|
||||||
|
label="Date d'expédition"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="col-start-1 row-start-4 h-[64px]">
|
||||||
|
<div class="flex w-full items-end gap-[104px]">
|
||||||
|
<UiRadioGroup
|
||||||
|
id="shipment-type"
|
||||||
|
name="shipment-type"
|
||||||
|
label="Type d'expédition bovine"
|
||||||
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
|
group-class="flex flex-row gap-[104px] w-[160px_160px] h-[32px]"
|
||||||
|
v-model="selectedShipmentTypeId"
|
||||||
|
:options="bovineShipment.map((type) => ({
|
||||||
|
value: String(type.id),
|
||||||
|
label: type.label
|
||||||
|
}))"
|
||||||
|
/>
|
||||||
|
<UiNumberInput
|
||||||
|
id="shipment-type-quantity"
|
||||||
|
v-model="shipmentQuantity"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="1200"
|
||||||
|
:disabled="!selectedShipmentTypeId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-customer"
|
||||||
|
v-model="form.customerId"
|
||||||
|
label="Client"
|
||||||
|
:options="customers.map((customer) => ({
|
||||||
|
value: String(customer.id),
|
||||||
|
label: customer.name || `Client #${customer.id}`
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingCustomers"
|
||||||
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
:options="customerAddressOptions"
|
||||||
|
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||||
|
label="Adresse"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-truck"
|
||||||
|
v-model="form.truckId"
|
||||||
|
label="Camion"
|
||||||
|
:options="trucks.map((truck) => ({
|
||||||
|
value: String(truck.id),
|
||||||
|
label: truck.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingTrucks"
|
||||||
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-carrier"
|
||||||
|
v-model="form.carrierId"
|
||||||
|
label="Transporteur"
|
||||||
|
:options="carriers.map((carrier) => ({
|
||||||
|
value: String(carrier.id),
|
||||||
|
label: carrier.name
|
||||||
|
}))"
|
||||||
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
id="shipment-vehicle"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="col-start-2 row-start-5 min-h-[72px]">
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
id="shipment-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="formIsLoading">
|
||||||
|
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
||||||
|
:class="activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weights'"
|
||||||
|
>
|
||||||
|
pesée à plein
|
||||||
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
|
||||||
|
:class="activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
|
||||||
|
@click="activeTab = 'weightsEmpty'"
|
||||||
|
>
|
||||||
|
pesée à vide
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mb-12">
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weights'"
|
||||||
|
v-model="grossWeight"
|
||||||
|
v-if="grossWeight"
|
||||||
|
:isAdmin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
<update-weight
|
||||||
|
v-show="activeTab === 'weightsEmpty'"
|
||||||
|
v-model="tareWeight"
|
||||||
|
v-if="tareWeight"
|
||||||
|
:isAdmin="authStore.isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UiButton
|
||||||
|
type="submit"
|
||||||
|
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
>
|
||||||
|
Valider
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePdfPrinter } from '#imports'
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import UpdateWeight from '~/components/commun/update-weight.vue'
|
||||||
|
import { getUsers } from '~/services/auth'
|
||||||
|
import { getCarrierList } from '~/services/carrier'
|
||||||
|
import { getCustomerList } from '~/services/customer'
|
||||||
|
import type { AddressData } from '~/services/dto/address-data'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import type { CustomerData } from '~/services/dto/customer-data'
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { ShipmentData, ShipmentFormData } from '~/services/dto/shipment-data'
|
||||||
|
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
||||||
|
import type { TruckData } from '~/services/dto/truck-data'
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
import type { WeightEntryData } from '~/services/dto/weight-data'
|
||||||
|
import { getDriverList } from '~/services/driver'
|
||||||
|
import { getShipment, updateShipment } from '~/services/shipment'
|
||||||
|
import { getShipmentTypeList } from '~/services/shipment-type'
|
||||||
|
import { getTruckList } from '~/services/truck'
|
||||||
|
import { getVehicleList } from '~/services/vehicle'
|
||||||
|
import { createWeight, updateWeight } from '~/services/weight'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
import { SUPPLIER_CODE } from '~/utils/constants'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const customers = ref<CustomerData[]>([])
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
|
const currentShipment = ref<ShipmentData | null>(null)
|
||||||
|
|
||||||
|
const selectedShipmentTypeId = ref('')
|
||||||
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const activeTab = ref<'weightsEmpty' | 'weights'>('weights')
|
||||||
|
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
||||||
|
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
||||||
|
const formIsLoading = ref(false)
|
||||||
|
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingShipmentTypes = ref(false)
|
||||||
|
const isLoadingCustomers = ref(false)
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
|
||||||
|
const form = reactive<ShipmentFormData & { identificationNumber: string | null }>({
|
||||||
|
identificationNumber: null,
|
||||||
|
userId: '',
|
||||||
|
shipmentDate: new Date().toISOString().slice(0, 10),
|
||||||
|
customerId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: '',
|
||||||
|
licensePlate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const shipmentId = computed(() => {
|
||||||
|
const id = Number(route.params.id)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
const isAddressData = (value: unknown): value is AddressData =>
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
'id' in value &&
|
||||||
|
'fullAddress' in value
|
||||||
|
|
||||||
|
const customerAddresses = computed<AddressData[]>(() => {
|
||||||
|
if (!form.customerId) return []
|
||||||
|
const customerId = Number(form.customerId)
|
||||||
|
if (!Number.isFinite(customerId) || customerId <= 0) return []
|
||||||
|
|
||||||
|
const addresses = customers.value.find((c) => c.id === customerId)?.addresses ?? []
|
||||||
|
return addresses.filter(isAddressData)
|
||||||
|
})
|
||||||
|
|
||||||
|
const customerAddressOptions = computed(() =>
|
||||||
|
customerAddresses.value.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return vehicles.value.filter(
|
||||||
|
(vehicle) =>
|
||||||
|
String(vehicle.carrier?.id) === form.carrierId &&
|
||||||
|
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadShipmentType = async () => {
|
||||||
|
isLoadingShipmentTypes.value = true
|
||||||
|
try {
|
||||||
|
bovineShipment.value = await getShipmentTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingShipmentTypes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCustomers = async () => {
|
||||||
|
isLoadingCustomers.value = true
|
||||||
|
try {
|
||||||
|
customers.value = await getCustomerList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCustomers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTrucks = async () => {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCarriers = async () => {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultUser() {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateFromShipment(shipment: ShipmentData | null) {
|
||||||
|
if (!shipment) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isHydrating.value = true
|
||||||
|
form.identificationNumber = shipment.identificationNumber ?? null
|
||||||
|
form.licensePlate = shipment.licensePlate ?? ''
|
||||||
|
form.shipmentDate = shipment.shipmentDate ?? new Date().toISOString().slice(0, 10)
|
||||||
|
form.userId = shipment.user?.id ? String(shipment.user.id) : form.userId
|
||||||
|
form.customerId = shipment.customer?.id ? String(shipment.customer.id) : ''
|
||||||
|
form.addressId = shipment.address?.id ? String(shipment.address.id) : ''
|
||||||
|
form.truckId = shipment.truck?.id ? String(shipment.truck.id) : ''
|
||||||
|
form.carrierId = shipment.carrier?.id ? String(shipment.carrier.id) : ''
|
||||||
|
form.driverId = shipment.driver?.id ? String(shipment.driver.id) : ''
|
||||||
|
form.vehicleId = shipment.vehicle?.id ? String(shipment.vehicle.id) : ''
|
||||||
|
selectedShipmentTypeId.value = shipment.shipmentType?.id ? String(shipment.shipmentType.id) : ''
|
||||||
|
shipmentQuantity.value = shipment.nbBovinSend ?? 0
|
||||||
|
const gross = shipment.weights?.find((weight) => weight.type === 'gross') ?? null
|
||||||
|
const tare = shipment.weights?.find((weight) => weight.type === 'tare') ?? null
|
||||||
|
grossWeight.value = gross ? { ...gross } : createEmptyWeightEntry('gross')
|
||||||
|
tareWeight.value = tare ? { ...tare } : createEmptyWeightEntry('tare')
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printReceipt() {
|
||||||
|
if (!import.meta.client || shipmentId.value === null || shipmentId.value <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerName =
|
||||||
|
customers.value.find((customer) => String(customer.id) === form.customerId)?.name ??
|
||||||
|
'client'
|
||||||
|
const filename = `${form.identificationNumber || shipmentId.value}_${customerName}_${form.licensePlate || 'immat'}.pdf`
|
||||||
|
await printPdf(`/shipments/${shipmentId.value}/receipt`, filename)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadShipmentForUpdate() {
|
||||||
|
if (shipmentId.value === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const shipment = await getShipment(shipmentId.value)
|
||||||
|
currentShipment.value = shipment
|
||||||
|
hydrateFromShipment(shipment)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.customerId, form.addressId, customers.value],
|
||||||
|
() => {
|
||||||
|
if (!form.customerId) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = customerAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function applyLiotDefaults() {
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.carrierId) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredDrivers.value.length === 1) {
|
||||||
|
form.driverId = String(filteredDrivers.value[0].id)
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
applyLiotDefaults()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isHydrating.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) {
|
||||||
|
applyLiotDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.truckId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = filteredVehicles.value.some(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
form.vehicleId = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value || isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selected = filteredVehicles.value.find(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
form.licensePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, form.vehicleId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value || form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const match = filteredVehicles.value.find(
|
||||||
|
(vehicle) => vehicle.plate === form.licensePlate
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
form.vehicleId = String(match.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function buildPayload() {
|
||||||
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
|
const normalizedShipmentDate = form.shipmentDate.trim()
|
||||||
|
const normalizedCustomerId = form.customerId.trim()
|
||||||
|
const normalizedTruckId = form.truckId.trim()
|
||||||
|
const normalizedCarrierId = form.carrierId.trim()
|
||||||
|
const normalizedDriverId = form.driverId.trim()
|
||||||
|
const normalizedUserId = form.userId.trim()
|
||||||
|
const normalizedAddressId = form.addressId.trim()
|
||||||
|
const normalizedShipmentTypeId = selectedShipmentTypeId.value.trim()
|
||||||
|
|
||||||
|
const customerIri = normalizedCustomerId ? `/api/customers/${normalizedCustomerId}` : null
|
||||||
|
const truckIri = normalizedTruckId ? `/api/trucks/${normalizedTruckId}` : null
|
||||||
|
const carrierIri = normalizedCarrierId ? `/api/carriers/${normalizedCarrierId}` : null
|
||||||
|
const userIri = normalizedUserId ? `/api/users/${normalizedUserId}` : null
|
||||||
|
const driverIri = normalizedDriverId ? `/api/drivers/${normalizedDriverId}` : null
|
||||||
|
const addressIri = normalizedAddressId ? `/api/addresses/${normalizedAddressId}` : null
|
||||||
|
const shipmentTypeIri = normalizedShipmentTypeId
|
||||||
|
? `/api/shipment_types/${normalizedShipmentTypeId}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
const rawQuantity = Number(shipmentQuantity.value ?? 0)
|
||||||
|
const normalizedQuantity = Number.isFinite(rawQuantity)
|
||||||
|
? Math.max(0, Math.trunc(rawQuantity))
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
licensePlate: normalizedLicensePlate,
|
||||||
|
shipmentDate: normalizedShipmentDate,
|
||||||
|
customer: customerIri,
|
||||||
|
truck: truckIri,
|
||||||
|
carrier: carrierIri,
|
||||||
|
driver: driverIri,
|
||||||
|
user: userIri,
|
||||||
|
address: addressIri,
|
||||||
|
shipmentType: shipmentTypeIri,
|
||||||
|
nbBovinSend: normalizedQuantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyWeightEntry(type: 'gross' | 'tare'): WeightEntryData {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
dsd: null,
|
||||||
|
weight: null,
|
||||||
|
weighedAt: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveWeightEntry(entry: WeightEntryData) {
|
||||||
|
if (!shipmentId.value || entry.weight === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: entry.type,
|
||||||
|
dsd: entry.dsd ?? null,
|
||||||
|
weight: entry.weight,
|
||||||
|
weighedAt: entry.weighedAt ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.id) {
|
||||||
|
await updateWeight(entry.id, payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createWeight({
|
||||||
|
shipment: `api/shipments/${shipmentId.value}`,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (shipmentId.value === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateShipment(shipmentId.value, {
|
||||||
|
currentStep: currentShipment.value?.currentStep ?? 0,
|
||||||
|
...buildPayload()
|
||||||
|
})
|
||||||
|
await saveWeightEntry(grossWeight.value)
|
||||||
|
await saveWeightEntry(tareWeight.value)
|
||||||
|
|
||||||
|
await loadShipmentForUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadShipmentType()
|
||||||
|
await loadUsers()
|
||||||
|
await loadCustomers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadVehicles()
|
||||||
|
await loadDrivers()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
formIsLoading.value = true
|
||||||
|
setDefaultUser()
|
||||||
|
await loadShipmentForUpdate()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
74
frontend/pages/shipment/waiting-shipment.vue
Normal file
74
frontend/pages/shipment/waiting-shipment.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions en attente</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px]">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
|
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Client</div>
|
||||||
|
<div>Adresse</div>
|
||||||
|
<div>Type d'expéditions</div>
|
||||||
|
<div>Transporteur</div>
|
||||||
|
<div>Immatriculation</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="shipment in shipmentList"
|
||||||
|
:key="shipment.id"
|
||||||
|
class="grid grid-cols-5 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToShipment(shipment.id)"
|
||||||
|
@keydown.enter="goToShipment(shipment.id)"
|
||||||
|
>
|
||||||
|
<div>{{ shipment.customer?.name }}</div>
|
||||||
|
<div>{{ shipment.address?.fullAddress }}</div>
|
||||||
|
<div>
|
||||||
|
<template v-if="formatShipmentLines(shipment).length">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in formatShipmentLines(shipment)"
|
||||||
|
:key="index"
|
||||||
|
class="leading-5"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>{{ shipment.carrier?.name }}</div>
|
||||||
|
<div>{{ shipment.licensePlate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
import {getShipmentList} from "~/services/shipment";
|
||||||
|
|
||||||
|
const shipmentList = ref<ShipmentData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goToShipment = (id: number) => {
|
||||||
|
router.push(`/shipment/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatShipmentLines = (shipment: ShipmentData) => {
|
||||||
|
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = typeof shipment.shipmentType === 'string'
|
||||||
|
? shipment.shipmentType
|
||||||
|
: shipment.shipmentType?.label
|
||||||
|
|
||||||
|
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
shipmentList.value = await getShipmentList(false)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
45
frontend/services/address.ts
Normal file
45
frontend/services/address.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type { AddressData } from '~/services/dto/address-data'
|
||||||
|
export interface AddressPayload {
|
||||||
|
label: string
|
||||||
|
street: string
|
||||||
|
street2?: string | null
|
||||||
|
postalCode: string
|
||||||
|
city: string
|
||||||
|
countryCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressData extends AddressPayload {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAddress(
|
||||||
|
payload: AddressPayload
|
||||||
|
): Promise<AddressData> {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return await api.post<AddressData>('addresses', payload, {
|
||||||
|
toastErrorKey: 'errors.address.create',
|
||||||
|
toastSuccessKey: 'success.address.create',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAddress(
|
||||||
|
id: number,
|
||||||
|
payload: AddressPayload
|
||||||
|
): Promise<AddressData> {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return await api.patch<AddressData>(`addresses/${id}`, payload, {
|
||||||
|
toastErrorKey: 'errors.address.update',
|
||||||
|
toastSuccessKey: 'success.address.update',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAddress(id: number): Promise<AddressData> {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return await api.get<AddressData>(`addresses/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.address.fetch',
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type { UserData } from '~/services/dto/user-data'
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type {UserPayload} from "~/services/dto/user-data";
|
||||||
|
|
||||||
export async function getUsers() {
|
export async function getUsers() {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
@@ -12,7 +13,40 @@ export async function getUsers() {
|
|||||||
|
|
||||||
return data['hydra:member'] ?? []
|
return data['hydra:member'] ?? []
|
||||||
}
|
}
|
||||||
|
export async function getAdminUsers() {
|
||||||
|
const api = useApi()
|
||||||
|
const data = await api.get<UserData[] | { 'hydra:member': UserData[] }>('admin/users', {}, {
|
||||||
|
toastErrorKey: 'errors.auth.users'
|
||||||
|
})
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
return data['hydra:member'] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUser(id: number) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<UserData>(`users/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.auth.user'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(payload: UserPayload = {}) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<UserData>('users', payload, {
|
||||||
|
toastErrorKey: 'errors.auth.create',
|
||||||
|
toastSuccessKey : 'success.auth.create'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(id : number, playload: UserPayload = {}){
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<UserData>(`users/${id}`, playload, {
|
||||||
|
toastErrorKey: 'errors.auth.update',
|
||||||
|
toastSuccessKey: 'success.auth.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
export async function getCurrentUser() {
|
export async function getCurrentUser() {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
return api.get<UserData>('me', {}, {
|
return api.get<UserData>('me', {}, {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type {BovineTypeData} from "~/services/dto/bovine-type-data";
|
import type { BovineTypeData, BovinPayload } from "~/services/dto/bovine-type-data";
|
||||||
|
|
||||||
export type BovineTypeListResponse =
|
export type BovineTypeListResponse =
|
||||||
| BovineTypeData[]
|
| BovineTypeData[]
|
||||||
@@ -12,12 +12,49 @@ export async function getBovineTypeList(): Promise<BovineTypeData[]> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(response)) {
|
if (Array.isArray(response)) {
|
||||||
return response
|
return response.map(mapToBovineTypeData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||||
return response['hydra:member']
|
return response['hydra:member'].map(mapToBovineTypeData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getBovin(id: number): Promise<BovineTypeData> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<BovineTypeData>(`bovine_types/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.bovin.fetch'
|
||||||
|
})
|
||||||
|
return mapToBovineTypeData(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createBovin(payload: BovinPayload = {}): Promise<BovineTypeData> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.post<BovineTypeData>('bovine_types', toBovineTypePayload(payload), {
|
||||||
|
toastErrorKey: 'errors.bovin.create',
|
||||||
|
toastSuccessKey: 'success.bovin.create'
|
||||||
|
})
|
||||||
|
return mapToBovineTypeData(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBovin(id: number, payload: BovinPayload = {}): Promise<BovineTypeData> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.patch<BovineTypeData>(`bovine_types/${id}`, toBovineTypePayload(payload), {
|
||||||
|
toastErrorKey: 'errors.bovin.update',
|
||||||
|
toastSuccessKey: 'success.bovin.update'
|
||||||
|
})
|
||||||
|
return mapToBovineTypeData(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapToBovineTypeData = (item: BovineTypeData): BovineTypeData => ({
|
||||||
|
id: item.id,
|
||||||
|
label: item.label,
|
||||||
|
code: item.code
|
||||||
|
})
|
||||||
|
|
||||||
|
const toBovineTypePayload = (payload: BovinPayload): Partial<BovineTypeData> => ({
|
||||||
|
label: payload.label ?? undefined,
|
||||||
|
code: payload.code ?? undefined
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type { CarrierData } from '~/services/dto/carrier-data'
|
import type {CarrierData, CarrierPayload} from "~/services/dto/carrier-data";
|
||||||
|
|
||||||
export type CarrierListResponse =
|
export type CarrierListResponse =
|
||||||
| CarrierData[]
|
| CarrierData[]
|
||||||
@@ -21,3 +21,26 @@ export async function getCarrierList(): Promise<CarrierData[]> {
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCarrier(id: number) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<CarrierData>(`carriers/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.carrier.fetch'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCarrier(id: number, payload: CarrierPayload) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<CarrierData>(`carriers/${id}`, payload, {
|
||||||
|
toastErrorKey: 'errors.carrier.update',
|
||||||
|
toastSuccessKey: 'success.carrier.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCarrier(payload: CarrierPayload = {}) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<CarrierData>('carriers', payload, {
|
||||||
|
toastErrorKey: 'errors.carrier.create',
|
||||||
|
toastSuccessKey: 'success.carrier.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
43
frontend/services/customer.ts
Normal file
43
frontend/services/customer.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useApi } from "~/composables/useApi"
|
||||||
|
import type { CustomerData, CustomerPayload } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
|
export type CustomerListResponse =
|
||||||
|
| CustomerData[]
|
||||||
|
| { "hydra:member"?: CustomerData[] }
|
||||||
|
|
||||||
|
export async function getCustomerList(): Promise<CustomerData[]> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<CustomerListResponse>("customers", {}, {
|
||||||
|
toastErrorKey: "errors.customer.list",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(response)) return response
|
||||||
|
if (response && typeof response === "object" && Array.isArray(response["hydra:member"])) {
|
||||||
|
return response["hydra:member"]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCustomer(id: number): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<CustomerData>(`customers/${id}`, {}, {
|
||||||
|
toastErrorKey: "errors.customer.fetch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCustomer(id: number, payload: Partial<CustomerPayload>): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<CustomerData>(`customers/${id}`, payload, {
|
||||||
|
toastErrorKey: "errors.customer.update",
|
||||||
|
toastSuccessKey: "success.customer.update",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCustomer(payload: CustomerPayload): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<CustomerData>("customers", payload, {
|
||||||
|
toastErrorKey: "errors.customer.create",
|
||||||
|
toastSuccessKey: "success.customer.create",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,5 +6,15 @@ export interface AddressData {
|
|||||||
postalCode: string
|
postalCode: string
|
||||||
city: string
|
city: string
|
||||||
countryCode: string
|
countryCode: string
|
||||||
fullAddress?: string
|
fullAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressFormData {
|
||||||
|
id?: number | null
|
||||||
|
label: string
|
||||||
|
street: string
|
||||||
|
street2?: string | null
|
||||||
|
postalCode: string
|
||||||
|
city: string
|
||||||
|
countryCode: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,13 @@ export interface BovineTypeData{
|
|||||||
label: string
|
label: string
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BovinFormData {
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BovinPayload = {
|
||||||
|
label?: string | null
|
||||||
|
code?: string | null
|
||||||
|
}
|
||||||
|
|||||||
9
frontend/services/dto/building-case-data.ts
Normal file
9
frontend/services/dto/building-case-data.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { BuildingCaseStatusData } from '~/services/dto/building-case-status-data'
|
||||||
|
|
||||||
|
export interface BuildingCaseData {
|
||||||
|
id: number
|
||||||
|
caseNumber: number | null
|
||||||
|
code: string | null
|
||||||
|
capacity: number | null
|
||||||
|
statut?: BuildingCaseStatusData | null
|
||||||
|
}
|
||||||
11
frontend/services/dto/building-case-position-data.ts
Normal file
11
frontend/services/dto/building-case-position-data.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { BuildingCaseData } from '~/services/dto/building-case-data'
|
||||||
|
|
||||||
|
export interface BuildingCasePositionData {
|
||||||
|
id: number
|
||||||
|
x: number | null
|
||||||
|
y: number | null
|
||||||
|
w: number | null
|
||||||
|
h: number | null
|
||||||
|
renderOrder: string | null
|
||||||
|
buildingCase: BuildingCaseData | null
|
||||||
|
}
|
||||||
6
frontend/services/dto/building-case-status-data.ts
Normal file
6
frontend/services/dto/building-case-status-data.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface BuildingCaseStatusData {
|
||||||
|
id: number
|
||||||
|
label: string | null
|
||||||
|
code: string | null
|
||||||
|
couleur: string | null
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import type { BuildingLayoutData } from '~/services/dto/building-layout-data'
|
||||||
|
|
||||||
export interface BuildingData {
|
export interface BuildingData {
|
||||||
id: number
|
id: number
|
||||||
label: string
|
label: string
|
||||||
code: string
|
code: string
|
||||||
|
layouts?: BuildingLayoutData[] | null
|
||||||
}
|
}
|
||||||
|
|||||||
9
frontend/services/dto/building-layout-data.ts
Normal file
9
frontend/services/dto/building-layout-data.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { BuildingCasePositionData } from '~/services/dto/building-case-position-data'
|
||||||
|
|
||||||
|
export interface BuildingLayoutData {
|
||||||
|
id: number
|
||||||
|
name: string | null
|
||||||
|
columns: number | null
|
||||||
|
rows: number | null
|
||||||
|
casePositions?: BuildingCasePositionData[] | null
|
||||||
|
}
|
||||||
@@ -3,3 +3,13 @@ export interface CarrierData {
|
|||||||
name: string
|
name: string
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CarrierFormData {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CarrierPayload = {
|
||||||
|
name?: string | null
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|||||||
25
frontend/services/dto/customer-data.ts
Normal file
25
frontend/services/dto/customer-data.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
|
||||||
|
export type CustomerAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
|
export interface CustomerData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
addresses: CustomerAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerFormData {
|
||||||
|
name: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
addresses: AddressFormData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerPayload = {
|
||||||
|
name: string
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
addresses?: string[]
|
||||||
|
}
|
||||||
@@ -41,6 +41,23 @@ export interface WeightEntryData {
|
|||||||
weighedAt: string | null
|
weighedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MerchandiseEntryData {
|
||||||
|
merchandiseTypeId: string
|
||||||
|
merchandiseDetail: string
|
||||||
|
selectedBuildingIds: string[]
|
||||||
|
selectedPelletBuildingIds: Record<string, string[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeightFormData {
|
||||||
|
id: number
|
||||||
|
weight: number
|
||||||
|
weighedAt : string
|
||||||
|
dsd: number
|
||||||
|
type: 'gross' | 'tare'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type ReceptionPayload = {
|
export type ReceptionPayload = {
|
||||||
licensePlate?: string | null
|
licensePlate?: string | null
|
||||||
receptionDate?: string
|
receptionDate?: string
|
||||||
@@ -59,3 +76,29 @@ export type ReceptionPayload = {
|
|||||||
carrier?: string | null
|
carrier?: string | null
|
||||||
driver?: string | null
|
driver?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReceptionFormData = {
|
||||||
|
identificationNumber?: null|string,
|
||||||
|
licensePlate: string
|
||||||
|
receptionDate: string
|
||||||
|
receptionTypeId: string
|
||||||
|
userId: string
|
||||||
|
supplierId: string
|
||||||
|
addressId: string
|
||||||
|
truckId: string
|
||||||
|
carrierId: string
|
||||||
|
driverId: string
|
||||||
|
vehicleId: string
|
||||||
|
weight?: ReceptionFormWeight | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReceptionFormWeight = {
|
||||||
|
weights: WeightFormData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceptionUpdatePayload {
|
||||||
|
weights: {
|
||||||
|
id: number
|
||||||
|
weight: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|||||||
68
frontend/services/dto/shipment-data.ts
Normal file
68
frontend/services/dto/shipment-data.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import type {CarrierData} from '~/services/dto/carrier-data'
|
||||||
|
import type {TruckData} from '~/services/dto/truck-data'
|
||||||
|
import type {CustomerData} from '~/services/dto/customer-data'
|
||||||
|
import type {AddressData} from "~/services/dto/address-data";
|
||||||
|
import type {UserData} from '~/services/dto/user-data'
|
||||||
|
import type {DriverData} from '~/services/dto/driver-data'
|
||||||
|
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||||
|
|
||||||
|
export interface ShipmentTypeData {
|
||||||
|
id: number
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShipmentData = {
|
||||||
|
id: number
|
||||||
|
identificationNumber?: string | null
|
||||||
|
licensePlate: string | null
|
||||||
|
shipmentDate: string
|
||||||
|
currentStep: number
|
||||||
|
isValid: boolean
|
||||||
|
address?: AddressData | null
|
||||||
|
carrier?: CarrierData | null
|
||||||
|
truck?: TruckData | null
|
||||||
|
customer?: CustomerData | null
|
||||||
|
user?: UserData | null
|
||||||
|
driver?: DriverData | null
|
||||||
|
vehicle?: VehicleData | null
|
||||||
|
shipmentType?: ShipmentTypeData | null
|
||||||
|
nbBovinSend?: number | null
|
||||||
|
weights?: WeightShipmentEntryData[] | null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeightShipmentEntryData {
|
||||||
|
id?: number
|
||||||
|
type: 'gross' | 'tare'
|
||||||
|
dsd: number | null
|
||||||
|
weight: number | null
|
||||||
|
weighedAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShipmentFormData = {
|
||||||
|
userId: string,
|
||||||
|
shipmentDate: string,
|
||||||
|
customerId: string,
|
||||||
|
addressId: string,
|
||||||
|
truckId: string,
|
||||||
|
carrierId: string,
|
||||||
|
driverId: string,
|
||||||
|
vehicleId: string,
|
||||||
|
licensePlate: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShipmentPayload = {
|
||||||
|
licensePlate?: string | null
|
||||||
|
shipmentDate?: string
|
||||||
|
currentStep?: number
|
||||||
|
isValid?: boolean
|
||||||
|
carrier?: string | null
|
||||||
|
truck?: string | null
|
||||||
|
customer?: string | null
|
||||||
|
address?: string | null
|
||||||
|
user?: string | null
|
||||||
|
driver?: string | null
|
||||||
|
shipmentType?: string | null
|
||||||
|
nbBovinSend?: number | null
|
||||||
|
}
|
||||||
5
frontend/services/dto/shipment-type-data.ts
Normal file
5
frontend/services/dto/shipment-type-data.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ShipmentTypeData {
|
||||||
|
id: number
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
@@ -1,9 +1,25 @@
|
|||||||
import type { AddressData } from '~/services/dto/address-data'
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
|
||||||
|
export type SupplierAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
export interface SupplierData {
|
export interface SupplierData {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
email?: string | null
|
email?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
addresses?: AddressData[] | null
|
addresses: SupplierAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupplierFormData {
|
||||||
|
name: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
addresses: AddressFormData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SupplierPayload = {
|
||||||
|
name: string
|
||||||
|
email?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
addresses?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
export interface UserData {
|
export interface UserData {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
|
roles: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserPayload = {
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
roles?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserFormData = {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
role: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,13 @@ export interface WeightData {
|
|||||||
weight: number | null
|
weight: number | null
|
||||||
dsd: number | null
|
dsd: number | null
|
||||||
weighedAt: string | null
|
weighedAt: string | null
|
||||||
|
type : string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeightEntryData {
|
||||||
|
id?: number
|
||||||
|
type: 'gross' | 'tare'
|
||||||
|
dsd: number | null
|
||||||
|
weight: number | null
|
||||||
|
weighedAt: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export async function getReception(id: number) {
|
|||||||
export async function createReception(payload: ReceptionPayload = {}) {
|
export async function createReception(payload: ReceptionPayload = {}) {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
return api.post<ReceptionData>('receptions', payload, {
|
return api.post<ReceptionData>('receptions', payload, {
|
||||||
|
toastSuccessKey: 'success.reception.create',
|
||||||
toastErrorKey: 'errors.reception.create'
|
toastErrorKey: 'errors.reception.create'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
24
frontend/services/shipment-type.ts
Normal file
24
frontend/services/shipment-type.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
|
||||||
|
|
||||||
|
export type ShipmentTypeListResponse =
|
||||||
|
| ShipmentTypeData[]
|
||||||
|
| { 'hydra:member'?: ShipmentTypeData[] }
|
||||||
|
|
||||||
|
|
||||||
|
export async function getShipmentTypeList(): Promise<ShipmentTypeData[]> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<ShipmentTypeListResponse>('shipment_types', {}, {
|
||||||
|
toastErrorKey: 'errors.shipmentType.list'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||||
|
return response['hydra:member']
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
42
frontend/services/shipment.ts
Normal file
42
frontend/services/shipment.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import {useApi} from '~/composables/useApi'
|
||||||
|
import type {ShipmentData, ShipmentPayload} from '~/services/dto/shipment-data'
|
||||||
|
import type {WeightData} from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
export async function getShipmentList(isValid: boolean|null = null) {
|
||||||
|
const api = useApi()
|
||||||
|
const query = isValid !== null ? { isValid: isValid} : {}
|
||||||
|
return api.get<ShipmentData[]>('shipments', query, {
|
||||||
|
toastErrorKey: 'errors.shipment.list'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getShipment(id: number) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<ShipmentData>(`shipments/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.shipment.fetch'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createShipment(payload: ShipmentPayload = {}) {
|
||||||
|
console.log('test')
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<ShipmentData>('shipments', payload, {
|
||||||
|
toastSuccessKey: 'success.shipment.create',
|
||||||
|
toastErrorKey: 'errors.shipment.create'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateShipment(id: number, payload: ShipmentPayload) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<ShipmentData>(`shipments/${id}`, payload, {
|
||||||
|
toastErrorKey: 'errors.shipment.update',
|
||||||
|
toastSuccessKey: 'success.shipment.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWeightShipment(): Promise<WeightData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<WeightData>('shipments/weigh', {}, {
|
||||||
|
toastErrorKey: 'errors.shipment.weigh'
|
||||||
|
})
|
||||||
|
}
|
||||||
23
frontend/services/statut.ts
Normal file
23
frontend/services/statut.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type { BuildingCaseStatusData } from '~/services/dto/building-case-status-data'
|
||||||
|
|
||||||
|
export type StatutListResponse =
|
||||||
|
| BuildingCaseStatusData[]
|
||||||
|
| { 'hydra:member'?: BuildingCaseStatusData[] }
|
||||||
|
|
||||||
|
export async function getStatutList(): Promise<BuildingCaseStatusData[]> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<StatutListResponse>('statuts', {}, {
|
||||||
|
toastErrorKey: 'errors.http.get'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||||
|
return response['hydra:member']
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from "~/composables/useApi"
|
||||||
import type { SupplierData } from '~/services/dto/supplier-data'
|
import type { SupplierData, SupplierPayload } from "~/services/dto/supplier-data"
|
||||||
|
|
||||||
export type SupplierListResponse =
|
export type SupplierListResponse =
|
||||||
| SupplierData[]
|
| SupplierData[]
|
||||||
| { 'hydra:member'?: SupplierData[] }
|
| { "hydra:member"?: SupplierData[] }
|
||||||
|
|
||||||
export async function getSupplierList(): Promise<SupplierData[]> {
|
export async function getSupplierList(): Promise<SupplierData[]> {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const response = await api.get<SupplierListResponse>('suppliers', {}, {
|
const response = await api.get<SupplierListResponse>("suppliers", {}, {
|
||||||
toastErrorKey: 'errors.supplier.list'
|
toastErrorKey: "errors.supplier.list",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(response)) {
|
if (Array.isArray(response)) return response
|
||||||
return response
|
if (response && typeof response === "object" && Array.isArray(response["hydra:member"])) {
|
||||||
|
return response["hydra:member"]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
|
||||||
return response['hydra:member']
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSupplier(id: number): Promise<SupplierData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<SupplierData>(`suppliers/${id}`, {}, {
|
||||||
|
toastErrorKey: "errors.supplier.fetch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSupplier(id: number, payload: Partial<SupplierPayload>): Promise<SupplierData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<SupplierData>(`suppliers/${id}`, payload, {
|
||||||
|
toastErrorKey: "errors.supplier.update",
|
||||||
|
toastSuccessKey: "success.supplier.update",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSupplier(payload: SupplierPayload): Promise<SupplierData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<SupplierData>("suppliers", payload, {
|
||||||
|
toastErrorKey: "errors.supplier.create",
|
||||||
|
toastSuccessKey: "success.supplier.create",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type { WeightEntryData } from '~/services/dto/reception-data'
|
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
|
||||||
|
import type {Ref} from "vue";
|
||||||
|
import type {ShipmentData, ShipmentPayload} from "~/services/dto/shipment-data";
|
||||||
|
import type {WeighingMode} from "~/composables/useWeighing";
|
||||||
|
|
||||||
export type WeightPayload = {
|
export type WeightPayload = {
|
||||||
reception: string
|
reception?: string
|
||||||
|
shipment?: string
|
||||||
type: 'gross' | 'tare'
|
type: 'gross' | 'tare'
|
||||||
dsd: number | null
|
dsd: number | null
|
||||||
weight: number | null
|
weight: number | null
|
||||||
@@ -16,5 +20,22 @@ export async function createWeight(payload: WeightPayload) {
|
|||||||
|
|
||||||
export async function updateWeight(id: number, payload: Partial<WeightPayload>) {
|
export async function updateWeight(id: number, payload: Partial<WeightPayload>) {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
return api.patch<WeightEntryData>(`weights/${id}`, payload)
|
return api.patch<WeightEntryData>(`weights/${id}`, payload,{
|
||||||
|
toastErrorKey: 'errors.weight.update',
|
||||||
|
toastSuccessKey: 'success.weight.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseWeighingShipmentOptions = {
|
||||||
|
modeShipment: WeighingMode
|
||||||
|
shipment: Ref<ShipmentData | null>
|
||||||
|
updateShipment: (id: number, payload: ShipmentPayload) => Promise<ShipmentData | null>
|
||||||
|
loadShipment?: (id: number) => Promise<ShipmentData | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseWeighingOptions = {
|
||||||
|
mode: WeighingMode
|
||||||
|
reception: Ref<ReceptionData | null>
|
||||||
|
updateReception: (id: number, payload: ReceptionPayload) => Promise<ReceptionData | null>
|
||||||
|
loadReception?: (id: number) => Promise<ReceptionData | null>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,80 @@
|
|||||||
import { defineStore } from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import type { UserData } from '~/services/dto/user-data'
|
import type {UserData} from '~/services/dto/user-data'
|
||||||
import { getCurrentUser, login, logout } from '~/services/auth'
|
import {getCurrentUser, createUser, login, logout} from '~/services/auth'
|
||||||
|
import type {UserPayload} from "~/services/dto/user-data";
|
||||||
|
import {ROLE} from '~/utils/constants'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
user: null as UserData | null,
|
user: null as UserData | null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
checked: false
|
checked: false
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
isAuthenticated: (state) => Boolean(state.user)
|
isAuthenticated: (state) => Boolean(state.user),
|
||||||
},
|
isAdmin: (state) => Boolean(state.user?.roles?.includes(ROLE[0].value))
|
||||||
actions: {
|
|
||||||
clearSession() {
|
|
||||||
this.user = null
|
|
||||||
this.checked = true
|
|
||||||
this.isLoading = false
|
|
||||||
},
|
},
|
||||||
async ensureSession() {
|
actions: {
|
||||||
if (this.checked) {
|
clearSession() {
|
||||||
return this.user
|
this.user = null
|
||||||
}
|
this.checked = true
|
||||||
|
this.isLoading = false
|
||||||
|
},
|
||||||
|
async ensureSession() {
|
||||||
|
if (this.checked) {
|
||||||
|
return this.user
|
||||||
|
}
|
||||||
|
|
||||||
this.checked = true
|
this.checked = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const me = await getCurrentUser()
|
const me = await getCurrentUser()
|
||||||
this.user = me
|
this.user = me
|
||||||
return me
|
return me
|
||||||
} catch {
|
} catch {
|
||||||
this.user = null
|
this.user = null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async login(username: string, password: string) {
|
async login(username: string, password: string) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await login(username, password)
|
await login(username, password)
|
||||||
const me = await getCurrentUser()
|
const me = await getCurrentUser()
|
||||||
this.user = me
|
this.user = me
|
||||||
this.checked = true
|
this.checked = true
|
||||||
return me
|
return me
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async logout() {
|
async createUser(payload: UserPayload = {}) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
const result = await createUser(payload).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
async updateUser(id: number, payload: UserPayload) {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = await createUser(payload).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
async logout() {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await logout()
|
await logout()
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore logout errors so we can still clear local auth state.
|
// Ignore logout errors so we can still clear local auth state.
|
||||||
} finally {
|
} finally {
|
||||||
this.user = null
|
this.user = null
|
||||||
this.checked = true
|
this.checked = true
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
58
frontend/stores/shipment.ts
Normal file
58
frontend/stores/shipment.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type {ShipmentData, ShipmentPayload} from "~/services/dto/shipment-data";
|
||||||
|
import {createShipment, getShipment, updateShipment} from "~/services/shipment";
|
||||||
|
|
||||||
|
const isShipmentData = (value: unknown): value is ShipmentData => {
|
||||||
|
return Boolean(value && typeof value === 'object' && 'id' in value)
|
||||||
|
}
|
||||||
|
export const useShipmentStore = defineStore('shipment', {
|
||||||
|
state: () => ({
|
||||||
|
current: null as ShipmentData | null,
|
||||||
|
isLoading: false
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setCurrent(shipment: ShipmentData | null) {
|
||||||
|
this.current = shipment
|
||||||
|
},
|
||||||
|
clearCurrent() {
|
||||||
|
this.current = null
|
||||||
|
},
|
||||||
|
async loadShipment(id: number) {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = await getShipment(id).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isShipmentData(result)) {
|
||||||
|
this.current = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
async createShipment(payload: ShipmentPayload = {}) {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = await createShipment(payload).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isShipmentData(result)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
async updateShipment(id: number, payload: ShipmentPayload) {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = await updateShipment(id, payload).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isShipmentData(result)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -8,17 +8,11 @@ export default <Partial<Config>>{
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
50: '#f6f9ea',
|
700: '#35453C',
|
||||||
100: '#eaf2cf',
|
500: '#456452',
|
||||||
200: '#d6e3a4',
|
|
||||||
300: '#c1d47a',
|
|
||||||
400: '#afc85a',
|
|
||||||
500: '#9ebb43',
|
|
||||||
600: '#7e9735',
|
|
||||||
700: '#607228',
|
|
||||||
800: '#414d1a',
|
|
||||||
900: '#24290d'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export const MERCHANDISE_TYPE_CODES = {
|
|||||||
AUTRES: 'AUTRES'
|
AUTRES: 'AUTRES'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const SUPLLIER_CODE = {
|
export const ROLE = [
|
||||||
|
{ label: 'Administrateur', value: 'ROLE_ADMIN' },
|
||||||
|
{ label: 'Utilisateur', value: 'ROLE_USER' }
|
||||||
|
]
|
||||||
|
export const SUPPLIER_CODE = {
|
||||||
LIOT: 'LIOT'
|
LIOT: 'LIOT'
|
||||||
}
|
}
|
||||||
|
|||||||
2
makefile
2
makefile
@@ -79,7 +79,7 @@ migration-migrate:
|
|||||||
$(SYMFONY_CONSOLE) --no-interaction doctrine:migrations:migrate --allow-no-migration
|
$(SYMFONY_CONSOLE) --no-interaction doctrine:migrations:migrate --allow-no-migration
|
||||||
|
|
||||||
fixtures:
|
fixtures:
|
||||||
$(SYMFONY_CONSOLE) doctrine:fixtures:load
|
$(SYMFONY_CONSOLE) --no-interaction doctrine:fixtures:load
|
||||||
|
|
||||||
# Attention, supprime votre bdd local
|
# Attention, supprime votre bdd local
|
||||||
db-reset:
|
db-reset:
|
||||||
|
|||||||
64
migrations/Version20260204101625.php
Normal file
64
migrations/Version20260204101625.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260204101625 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE bovin_shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, nb_bovin_send INT NOT NULL, shipment_id INT DEFAULT NULL, shipment_type_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_7049F4507BE036FC ON bovin_shipment (shipment_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_7049F4502EE48A36 ON bovin_shipment (shipment_type_id)');
|
||||||
|
$this->addSql('CREATE TABLE customer (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE TABLE customer_address (customer_id INT NOT NULL, address_id INT NOT NULL, PRIMARY KEY (customer_id, address_id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_1193CB3F9395C3F3 ON customer_address (customer_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_1193CB3FF5B7AF75 ON customer_address (address_id)');
|
||||||
|
$this->addSql('CREATE TABLE shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, licence_plate VARCHAR(255) NOT NULL, identification_number VARCHAR(20) DEFAULT NULL, current_step INT DEFAULT 0 NOT NULL, is_valid BOOLEAN NOT NULL, shipment_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, carrier_id INT DEFAULT NULL, vehicle_id INT DEFAULT NULL, truck_id INT DEFAULT NULL, customer_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_2CB20DC347639A5 ON shipment (identification_number)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DC21DFC797 ON shipment (carrier_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DC545317D1 ON shipment (vehicle_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCC6957CCE ON shipment (truck_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DC9395C3F3 ON shipment (customer_id)');
|
||||||
|
$this->addSql('CREATE TABLE shipment_type (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT FK_7049F4507BE036FC FOREIGN KEY (shipment_id) REFERENCES shipment (id)');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT FK_7049F4502EE48A36 FOREIGN KEY (shipment_type_id) REFERENCES shipment_type (id)');
|
||||||
|
$this->addSql('ALTER TABLE customer_address ADD CONSTRAINT FK_1193CB3F9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) ON DELETE CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE customer_address ADD CONSTRAINT FK_1193CB3FF5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id) ON DELETE CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC21DFC797 FOREIGN KEY (carrier_id) REFERENCES carrier (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC545317D1 FOREIGN KEY (vehicle_id) REFERENCES vehicle (id)');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCC6957CCE FOREIGN KEY (truck_id) REFERENCES truck (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT FK_7049F4507BE036FC');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT FK_7049F4502EE48A36');
|
||||||
|
$this->addSql('ALTER TABLE customer_address DROP CONSTRAINT FK_1193CB3F9395C3F3');
|
||||||
|
$this->addSql('ALTER TABLE customer_address DROP CONSTRAINT FK_1193CB3FF5B7AF75');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC21DFC797');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC545317D1');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCC6957CCE');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC9395C3F3');
|
||||||
|
$this->addSql('DROP TABLE bovin_shipment');
|
||||||
|
$this->addSql('DROP TABLE customer');
|
||||||
|
$this->addSql('DROP TABLE customer_address');
|
||||||
|
$this->addSql('DROP TABLE shipment');
|
||||||
|
$this->addSql('DROP TABLE shipment_type');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
migrations/Version20260204102423.php
Normal file
35
migrations/Version20260204102423.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260204102423 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT fk_2cb20dc545317d1');
|
||||||
|
$this->addSql('DROP INDEX idx_2cb20dc545317d1');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP vehicle_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD vehicle_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT fk_2cb20dc545317d1 FOREIGN KEY (vehicle_id) REFERENCES vehicle (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX idx_2cb20dc545317d1 ON shipment (vehicle_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
49
migrations/Version20260211075656.php
Normal file
49
migrations/Version20260211075656.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260211075656 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment ON bovin_shipment (shipment_id, shipment_type_id)');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD user_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD driver_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD address_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCA76ED395 FOREIGN KEY (user_id) REFERENCES public."user" (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCC3423909 FOREIGN KEY (driver_id) REFERENCES driver (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCF5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCA76ED395 ON shipment (user_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCC3423909 ON shipment (driver_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCF5B7AF75 ON shipment (address_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP INDEX uniq_bovin_shipment');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCA76ED395');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCC3423909');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCF5B7AF75');
|
||||||
|
$this->addSql('DROP INDEX IDX_2CB20DCA76ED395');
|
||||||
|
$this->addSql('DROP INDEX IDX_2CB20DCC3423909');
|
||||||
|
$this->addSql('DROP INDEX IDX_2CB20DCF5B7AF75');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP user_id');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP driver_id');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP address_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
38
migrations/Version20260211123000.php
Normal file
38
migrations/Version20260211123000.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260211123000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Allow weight to belong to reception or shipment.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE weight ALTER COLUMN reception_id DROP NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE weight ADD shipment_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE weight ADD CONSTRAINT FK_WEIGHT_SHIPMENT FOREIGN KEY (shipment_id) REFERENCES shipment (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_WEIGHT_SHIPMENT ON weight (shipment_id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_weight_reception_type ON weight (reception_id, type)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_weight_shipment_type ON weight (shipment_id, type)');
|
||||||
|
$this->addSql('ALTER TABLE weight ADD CONSTRAINT chk_weight_reception_or_shipment CHECK ((reception_id IS NOT NULL AND shipment_id IS NULL) OR (reception_id IS NULL AND shipment_id IS NOT NULL))');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE weight DROP CONSTRAINT chk_weight_reception_or_shipment');
|
||||||
|
$this->addSql('DROP INDEX uniq_weight_shipment_type');
|
||||||
|
$this->addSql('DROP INDEX uniq_weight_reception_type');
|
||||||
|
$this->addSql('DROP INDEX IDX_WEIGHT_SHIPMENT');
|
||||||
|
$this->addSql('ALTER TABLE weight DROP CONSTRAINT FK_WEIGHT_SHIPMENT');
|
||||||
|
$this->addSql('ALTER TABLE weight DROP shipment_id');
|
||||||
|
$this->addSql('ALTER TABLE weight ALTER COLUMN reception_id SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
migrations/Version20260213093000.php
Normal file
32
migrations/Version20260213093000.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213093000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add name, phone and email fields to customer.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ADD name VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD phone VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD email VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE customer SET name = label WHERE name IS NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN name SET NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer DROP name');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP phone');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP email');
|
||||||
|
}
|
||||||
|
}
|
||||||
37
migrations/Version20260213101500.php
Normal file
37
migrations/Version20260213101500.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213101500 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Align customer with supplier: keep name/email/phone and drop label/code.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN name TYPE VARCHAR(180)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(180)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(40)');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP COLUMN label');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP COLUMN code');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ADD label VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD code VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE customer SET label = name WHERE label IS NULL');
|
||||||
|
$this->addSql("UPDATE customer SET code = regexp_replace(upper(name), '[^A-Z0-9]+', '_', 'g') WHERE code IS NULL");
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN label SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN code SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(255)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(255)');
|
||||||
|
}
|
||||||
|
}
|
||||||
40
migrations/Version20260213114000.php
Normal file
40
migrations/Version20260213114000.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213114000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Allow only one bovin_shipment row per shipment.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Keep one row per shipment (latest id), required before adding unique index.
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DELETE FROM bovin_shipment bs
|
||||||
|
USING (
|
||||||
|
SELECT id, ROW_NUMBER() OVER (PARTITION BY shipment_id ORDER BY id DESC) AS rn
|
||||||
|
FROM bovin_shipment
|
||||||
|
WHERE shipment_id IS NOT NULL
|
||||||
|
) d
|
||||||
|
WHERE bs.id = d.id
|
||||||
|
AND d.rn > 1
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_bovin_shipment');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment_one_type ON bovin_shipment (shipment_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_bovin_shipment_one_type');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment ON bovin_shipment (shipment_id, shipment_type_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
52
migrations/Version20260218144828.php
Normal file
52
migrations/Version20260218144828.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260218144828 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT fk_7049f4502ee48a36');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT fk_7049f4507be036fc');
|
||||||
|
$this->addSql('DROP TABLE bovin_shipment');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD nb_bovin_send INT NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD shipment_type_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC2EE48A36 FOREIGN KEY (shipment_type_id) REFERENCES shipment_type (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DC2EE48A36 ON shipment (shipment_type_id)');
|
||||||
|
$this->addSql('DROP INDEX uniq_weight_shipment_type');
|
||||||
|
$this->addSql('DROP INDEX uniq_weight_reception_type');
|
||||||
|
$this->addSql('ALTER INDEX idx_weight_shipment RENAME TO IDX_7CD55417BE036FC');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE bovin_shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, nb_bovin_send INT NOT NULL, shipment_id INT DEFAULT NULL, shipment_type_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_7049f4507be036fc ON bovin_shipment (shipment_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_7049f4502ee48a36 ON bovin_shipment (shipment_type_id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment_one_type ON bovin_shipment (shipment_id)');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT fk_7049f4502ee48a36 FOREIGN KEY (shipment_type_id) REFERENCES shipment_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT fk_7049f4507be036fc FOREIGN KEY (shipment_id) REFERENCES shipment (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC2EE48A36');
|
||||||
|
$this->addSql('DROP INDEX IDX_2CB20DC2EE48A36');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP nb_bovin_send');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP shipment_type_id');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_weight_shipment_type ON weight (shipment_id, type)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_weight_reception_type ON weight (reception_id, type)');
|
||||||
|
$this->addSql('ALTER INDEX idx_7cd55417be036fc RENAME TO idx_weight_shipment');
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user