Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e707484a0 | ||
| 0067e51e6e | |||
|
|
1c0cdeb085 | ||
| 465339cdd6 | |||
|
|
2bc484574f | ||
| ea1e3b074c | |||
|
|
4944611088 | ||
| fbfc7acfe4 | |||
|
|
92f54f600f | ||
| a905c6a1de | |||
|
|
995e7de2cc | ||
| 2408ccab67 | |||
| 82af4d4c1e | |||
|
|
11491b02c5 | ||
| 024af5887e | |||
|
|
91c0125876 | ||
| b510cdcc42 | |||
|
|
d0213c3212 | ||
| 3ac676689d |
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"WebFetch(domain:geo.api.gouv.fr)",
|
||||||
|
"Bash(pip3 install:*)",
|
||||||
|
"Bash(python3 -c \":*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd frontend
|
||||||
npm ci
|
npm ci
|
||||||
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ npm run generate
|
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ NUXT_PUBLIC_GEO_API_BASE=https://geo.api.gouv.fr npm run generate
|
||||||
test -f .output/public/index.html
|
test -f .output/public/index.html
|
||||||
|
|
||||||
- name: Build artefact
|
- name: Build artefact
|
||||||
|
|||||||
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>
|
||||||
232
.idea/workspace.xml
generated
232
.idea/workspace.xml
generated
@@ -4,10 +4,12 @@
|
|||||||
<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 : front page admin bovin et changelog">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : order navbar + modification création fournisseur et client">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.claude/settings.local.json" beforeDir="false" afterPath="$PROJECT_DIR$/.claude/settings.local.json" 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/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/pages/admin/supplier/supplier-list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/Entity/Reception.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Reception.php" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/Entity/Shipment.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Shipment.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" />
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="feat/356-front-page-admin-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$" />
|
||||||
@@ -223,59 +225,60 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent">{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "feat/352-modification-front-admin-fournisseur",
|
"git-widget-placeholder": "develop",
|
||||||
"last_opened_file_path": "/home/sroy/Documents/test/Ferme/frontend/components/commun",
|
"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": "preferences.pluginManager",
|
"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"
|
||||||
},
|
},
|
||||||
"keyToStringList": {
|
"keyToStringList": {
|
||||||
"DatabaseDriversLRU": [
|
"DatabaseDriversLRU": [
|
||||||
"postgresql"
|
"postgresql"
|
||||||
],
|
],
|
||||||
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||||
"TEXT"
|
"TEXT"
|
||||||
],
|
],
|
||||||
"vue.recent.templates": [
|
"vue.recent.templates": [
|
||||||
"Vue Composition API Component"
|
"Vue Composition API Component"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}]]></component>
|
}</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="CopyFile.RECENT_KEYS">
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
<recent name="$PROJECT_DIR$/frontend/components/commun" />
|
|
||||||
<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="$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\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\composables" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\components\shipment" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\components\shipment" />
|
||||||
</key>
|
</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="C:\Users\m-tristan\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||||
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||||
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
|
|
||||||
</key>
|
</key>
|
||||||
</component>
|
</component>
|
||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
<set>
|
<set>
|
||||||
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.30387.85" />
|
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.32098.40" />
|
||||||
</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="" />
|
||||||
@@ -310,70 +313,20 @@
|
|||||||
<workItem from="1770879701502" duration="25805000" />
|
<workItem from="1770879701502" duration="25805000" />
|
||||||
<workItem from="1770966186589" duration="914000" />
|
<workItem from="1770966186589" duration="914000" />
|
||||||
<workItem from="1770967274060" duration="2388000" />
|
<workItem from="1770967274060" duration="2388000" />
|
||||||
</task>
|
<workItem from="1772466451823" duration="598000" />
|
||||||
<task id="LOCAL-00020" summary="ci : fix release artefact">
|
<workItem from="1772626984813" duration="969000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1772786360430" duration="21000" />
|
||||||
<created>1769024603812</created>
|
<workItem from="1772786475316" duration="3016000" />
|
||||||
<option name="number" value="00020" />
|
<workItem from="1773049125640" duration="406000" />
|
||||||
<option name="presentableId" value="LOCAL-00020" />
|
<workItem from="1773049540928" duration="539000" />
|
||||||
<option name="project" value="LOCAL" />
|
<workItem from="1773050154207" duration="1879000" />
|
||||||
<updated>1769024603812</updated>
|
<workItem from="1773212999001" duration="652000" />
|
||||||
</task>
|
<workItem from="1773215356754" duration="5754000" />
|
||||||
<task id="LOCAL-00021" summary="ci : ajout du script et de la doc déploiement">
|
<workItem from="1773756072697" duration="5450000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1773766075191" duration="6202000" />
|
||||||
<created>1769026716634</created>
|
<workItem from="1773824491213" duration="24805000" />
|
||||||
<option name="number" value="00021" />
|
<workItem from="1774275549972" duration="51000" />
|
||||||
<option name="presentableId" value="LOCAL-00021" />
|
<workItem from="1774276665015" duration="2660000" />
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769026716634</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00022" summary="fix : correction du path URI pour la création d'un poids dans une réception">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769073690382</created>
|
|
||||||
<option name="number" value="00022" />
|
|
||||||
<option name="presentableId" value="LOCAL-00022" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769073690382</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00023" summary="feat : Ajout du bundle Monolog pour la gestion des logs">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769075990984</created>
|
|
||||||
<option name="number" value="00023" />
|
|
||||||
<option name="presentableId" value="LOCAL-00023" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769075990984</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00024" summary="fix : affiche plus détail dans les logs en recette/prod">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769077633390</created>
|
|
||||||
<option name="number" value="00024" />
|
|
||||||
<option name="presentableId" value="LOCAL-00024" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769077633390</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00025" summary="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769079030808</created>
|
|
||||||
<option name="number" value="00025" />
|
|
||||||
<option name="presentableId" value="LOCAL-00025" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769079030808</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00026" summary="fix : doc de déploiement">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769094376813</created>
|
|
||||||
<option name="number" value="00026" />
|
|
||||||
<option name="presentableId" value="LOCAL-00026" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769094376813</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00027" summary="fix : doc et script de déploiement">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1769096187792</created>
|
|
||||||
<option name="number" value="00027" />
|
|
||||||
<option name="presentableId" value="LOCAL-00027" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1769096187792</updated>
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00028" summary="fix : doc et script de déploiement">
|
<task id="LOCAL-00028" summary="fix : doc et script de déploiement">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -703,7 +656,71 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1772182707441</updated>
|
<updated>1772182707441</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="69" />
|
<task id="LOCAL-00069" summary="fix : on ne bloque plus le poids max d'une pesée">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1772447581744</created>
|
||||||
|
<option name="number" value="00069" />
|
||||||
|
<option name="presentableId" value="LOCAL-00069" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1772447581744</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00070" summary="feat : ajout de supplier dans la feed et fixtures">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773761787472</created>
|
||||||
|
<option name="number" value="00070" />
|
||||||
|
<option name="presentableId" value="LOCAL-00070" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773761787472</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00071" summary="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773766207721</created>
|
||||||
|
<option name="number" value="00071" />
|
||||||
|
<option name="presentableId" value="LOCAL-00071" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773766207721</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00072" summary="fix : on ne pèse plus automatiquement + fix message de création réception">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773826699115</created>
|
||||||
|
<option name="number" value="00072" />
|
||||||
|
<option name="presentableId" value="LOCAL-00072" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773826699115</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00073" summary="fix : correction des retours de la V0">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773841634554</created>
|
||||||
|
<option name="number" value="00073" />
|
||||||
|
<option name="presentableId" value="LOCAL-00073" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773841634554</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00074" summary="feat : ajout de l'api de l'état pour chercher les villes via le CP">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773842791819</created>
|
||||||
|
<option name="number" value="00074" />
|
||||||
|
<option name="presentableId" value="LOCAL-00074" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773842791819</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00075" summary="fix : script de déploiement + CI/CD build de l'app">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773843922376</created>
|
||||||
|
<option name="number" value="00075" />
|
||||||
|
<option name="presentableId" value="LOCAL-00075" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773843922377</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00076" summary="fix : order navbar + modification création fournisseur et client">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773852806120</created>
|
||||||
|
<option name="number" value="00076" />
|
||||||
|
<option name="presentableId" value="LOCAL-00076" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773852806121</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="77" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -753,14 +770,6 @@
|
|||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception" />
|
|
||||||
<MESSAGE value="feat : mise en place de composant UI pour les select, checkbox, date, text" />
|
|
||||||
<MESSAGE value="feat : update CHANGELOG.md" />
|
|
||||||
<MESSAGE value="feat : ajout de commentaire" />
|
|
||||||
<MESSAGE value="fix : correction de l'affichage de l'immatriculation sur une réception en cours + correction css étape 3 d'une réception" />
|
|
||||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception" />
|
|
||||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
|
||||||
<MESSAGE value="feat : mise à jour du bon de réception" />
|
|
||||||
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||||
<MESSAGE value="feat : creer une nouvelle expedtion (WIP)" />
|
<MESSAGE value="feat : creer une nouvelle expedtion (WIP)" />
|
||||||
<MESSAGE value="feat : ajout d'une page de creation d'une expedition" />
|
<MESSAGE value="feat : ajout d'une page de creation d'une expedition" />
|
||||||
@@ -778,7 +787,15 @@
|
|||||||
<MESSAGE value="fix : espacement" />
|
<MESSAGE value="fix : espacement" />
|
||||||
<MESSAGE value="fix : text" />
|
<MESSAGE value="fix : text" />
|
||||||
<MESSAGE value="feat : front page admin bovin et changelog" />
|
<MESSAGE value="feat : front page admin bovin et changelog" />
|
||||||
<option name="LAST_COMMIT_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" />
|
||||||
|
<MESSAGE value="fix : on ne pèse plus automatiquement + fix message de création réception" />
|
||||||
|
<MESSAGE value="fix : correction des retours de la V0" />
|
||||||
|
<MESSAGE value="feat : ajout de l'api de l'état pour chercher les villes via le CP" />
|
||||||
|
<MESSAGE value="fix : script de déploiement + CI/CD build de l'app" />
|
||||||
|
<MESSAGE value="fix : order navbar + modification création fournisseur et client" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="fix : order navbar + modification création fournisseur et client" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XDebuggerManager">
|
<component name="XDebuggerManager">
|
||||||
<breakpoint-manager>
|
<breakpoint-manager>
|
||||||
@@ -794,9 +811,8 @@
|
|||||||
<option name="timeStamp" value="45" />
|
<option name="timeStamp" value="45" />
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
<line-breakpoint enabled="true" type="javascript">
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
<url>file://$PROJECT_DIR$/frontend/layouts/default.vue</url>
|
<url>file://$PROJECT_DIR$/frontend/services/dto/shipment-data.ts</url>
|
||||||
<line>72</line>
|
<option name="timeStamp" value="43" />
|
||||||
<option name="timeStamp" value="48" />
|
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
</breakpoints>
|
</breakpoints>
|
||||||
</breakpoint-manager>
|
</breakpoint-manager>
|
||||||
|
|||||||
73
AGENTS.md
73
AGENTS.md
@@ -1,73 +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.
|
|
||||||
- No custom repository classes are used (`src/Repository` removed); use default Doctrine repositories via `EntityManagerInterface`.
|
|
||||||
- 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.
|
|
||||||
- Bovine storage:
|
|
||||||
- `src/Entity/Bovine.php` with fields `nationalNumber` (unique), `receivedWeight`, `arrivalDate`, and `buildingCase` (ManyToOne).
|
|
||||||
- `src/Entity/BuildingCase.php` has `bovines` (OneToMany).
|
|
||||||
- Case PDF report:
|
|
||||||
- Endpoint: `GET /building_cases/{id}/weights-report` (provider: `App\State\BuildingCaseWeightsReportProvider`).
|
|
||||||
- Template: `templates/case_weights_report.html.twig`.
|
|
||||||
- Projection logic is done in backend from `arrivalDate`; daily gain is currently fixed at `1.3 kg/day` for all races.
|
|
||||||
|
|
||||||
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`.
|
|
||||||
- Infrastructure case page prints the case weight report PDF from `frontend/pages/infrastructure/case.vue` using `usePdfPrinter('/building_cases/{id}/weights-report')`.
|
|
||||||
|
|
||||||
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).
|
|
||||||
- Seed and fixtures conventions:
|
|
||||||
- `app:seed` now seeds infrastructure (`statut`, `building_layout`, `building_case`, `building_case_position`) and bovines.
|
|
||||||
- `app:seed` uses intermediate flushes (after buildings and after infrastructure) so find queries can resolve just-created records.
|
|
||||||
- Bovine seed rows use a legacy case token mapping to building-case code (`B{building}-C{case}`) before fallback to direct id lookup.
|
|
||||||
- Fixtures include `BuildingInfrastructureFixtures` + `BovineFixtures` (via `AppFixtures` dependencies).
|
|
||||||
- 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`.
|
|
||||||
@@ -58,6 +58,7 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* [#355] modification front admin transporteur
|
* [#355] modification front admin transporteur
|
||||||
* [#356] front page admin bovin
|
* [#356] front page admin bovin
|
||||||
* [#353] modification front admin client
|
* [#353] modification front admin client
|
||||||
|
* [#353] modification front admin utilisateur
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
160
CLAUDE.md
Normal file
160
CLAUDE.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Backend:** Symfony 8 + API Platform 4 (PHP 8.4)
|
||||||
|
- **Frontend:** Nuxt 4 (Vue 3, Pinia, Tailwind, Zod) in `frontend/`
|
||||||
|
- **Infra:** Docker (PHP-FPM + Nginx), Apache vhost serves API sous `/api` et frontend depuis `frontend/dist`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
make start # Démarrer les containers
|
||||||
|
make stop # Arrêter les containers
|
||||||
|
make restart # Redémarrer les containers
|
||||||
|
make shell # Shell dans le container PHP
|
||||||
|
|
||||||
|
# Install complet
|
||||||
|
make install # composer install + migrations + build frontend
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
make composer-install # Installer dépendances PHP
|
||||||
|
make migration-migrate # Lancer les migrations
|
||||||
|
make fixtures # Charger les fixtures
|
||||||
|
make cache-clear # Vider le cache Symfony
|
||||||
|
make test # Lancer les tests PHPUnit
|
||||||
|
make test FILES=tests/path/to/TestFile.php # Test spécifique
|
||||||
|
make php-cs-fixer-allow-risky FILES=src/... # Fixer le style
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
make build-nuxtJS # npm install + build:dist (dans le container)
|
||||||
|
make dev-nuxt # Serveur dev Nuxt (dans le container)
|
||||||
|
# Ou directement dans frontend/ :
|
||||||
|
cd frontend && npm run dev # Dev server (port 3000)
|
||||||
|
cd frontend && npm run build:dist # Build production
|
||||||
|
|
||||||
|
# Base de données
|
||||||
|
make db-reset # ⚠️ Supprime et recrée la BDD + migrations + fixtures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture backend
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── ApiResource/ # Ressources API Platform custom
|
||||||
|
├── Command/ # Commandes Symfony (dont app:seed)
|
||||||
|
├── DataFixtures/ # Fixtures Doctrine
|
||||||
|
├── Dto/ # DTOs (ex: PontBasculeReading)
|
||||||
|
├── Entity/ # Entités Doctrine (= ressources API Platform)
|
||||||
|
├── Exception/ # Exceptions custom (PontBasculeException)
|
||||||
|
├── Kernel.php
|
||||||
|
├── Service/ # Services métier (PontBasculePayloadDecoder…)
|
||||||
|
└── State/ # State providers/processors API Platform
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture frontend
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── components/
|
||||||
|
│ ├── ui/ # Composants réutilisables, auto-importés avec préfixe Ui (ex: UiLoadingDots)
|
||||||
|
│ ├── reception/ # Composants métier réception
|
||||||
|
│ ├── shipment/ # Composants métier expédition
|
||||||
|
│ ├── workflow/ # Composants partagés réception/expédition (workflow-weight, workflow-waiting-list, workflow-liot-fields)
|
||||||
|
│ └── commun/ # Composants communs (update-weight)
|
||||||
|
├── composables/ # useApi, useWeighing, usePdfPrinter, useAppVersion, useLiotHandling, useFormDataLoading, useAddressSync, useWorkflowSteps
|
||||||
|
│ └── steps/ # useWeighingStep (logique étape pesée)
|
||||||
|
├── config/ # reception.config.ts, shipment.config.ts (WorkflowConfig)
|
||||||
|
├── types/ # workflow.ts (interfaces partagées WorkflowEntity, WorkflowConfig, StepDefinition)
|
||||||
|
├── services/ # Couche service avec DTOs typés dans services/dto/
|
||||||
|
│ └── workflow-service.ts # Factory service API (createWorkflowService)
|
||||||
|
├── stores/ # Pinia stores (reception, shipment, auth)
|
||||||
|
│ └── workflow-store.ts # Factory store (useWorkflowStoreLogic)
|
||||||
|
├── pages/ # Pages Nuxt (file-based routing)
|
||||||
|
├── layouts/ # Layout default : max-width 1050px
|
||||||
|
├── i18n/locales/ # Traductions (défaut: fr)
|
||||||
|
├── utils/ # Constants, zod-errors helpers
|
||||||
|
└── assets/css/ # Tailwind config, main.css (font Helvetica)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions backend
|
||||||
|
|
||||||
|
- Code en anglais ; "pont-bascule" est un terme métier conservé tel quel.
|
||||||
|
- Les opérations API Platform sont définies directement sur les entités Doctrine.
|
||||||
|
- 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.
|
||||||
|
- Les noms de `Supplier`, `Customer` et `Carrier` sont automatiquement mis en majuscule via `mb_strtoupper` dans `setName()`.
|
||||||
|
|
||||||
|
## Conventions frontend
|
||||||
|
|
||||||
|
- SSR désactivé. Tailwind avec palette custom `primary` (ex: `bg-primary-500`).
|
||||||
|
- `useApi` (`composables/useApi.ts`) : méthodes `get/post/put/patch/delete` avec content-types par défaut.
|
||||||
|
- Toasts personnalisables via `toastErrorMessage`/`toastSuccessMessage` ou clés i18n `toastErrorKey`/`toastSuccessKey`.
|
||||||
|
- Utilise `useNuxtApp().$i18n` (pas `useI18n`) pour fonctionner hors setup.
|
||||||
|
- Validation formulaires avec Zod ; helpers dans `utils/zod-errors.ts`.
|
||||||
|
- Nav active : `NuxtLink` avec slot `custom`.
|
||||||
|
- PDFs : `usePdfPrinter` (receipt réception, rapport poids cases).
|
||||||
|
|
||||||
|
### Validation required & erreurs visuelles
|
||||||
|
- Les champs `required` utilisent l'attribut HTML natif forwardé via `v-bind="attrs"` dans les composants UI.
|
||||||
|
- La bordure rouge n'apparaît qu'après soumission grâce à la classe CSS `submitted` ajoutée sur le `<form>` au clic sur le bouton Valider (`@click="submitted = true"`).
|
||||||
|
- Règles CSS globales dans `main.css` : `.submitted :invalid` (bordure + texte rouge), `.submitted :has(:invalid) > label` et `.submitted label:has(:invalid)` (labels rouges).
|
||||||
|
- Pour les validations manuelles (checkboxes, radio groups), les messages d'erreur utilisent `invisible` (pas `hidden`) pour garder l'espace réservé et éviter les décalages de layout.
|
||||||
|
- Les dates de l'API sont renvoyées au format `Y-m-d H:i` ; les formulaires utilisent `.slice(0, 10)` pour extraire la date seule (compatible `<input type="date">`).
|
||||||
|
|
||||||
|
### Workflow réception/expédition (mutualisé)
|
||||||
|
- Factory service `createWorkflowService` et factory store `useWorkflowStoreLogic` pour éviter la duplication.
|
||||||
|
- Composables partagés : `useLiotHandling` (logique LIOT), `useFormDataLoading` (users, trucks, carriers), `useAddressSync` (sync adresse fournisseur/client).
|
||||||
|
- `useWeighing` : une seule fonction paramétrée pour réception et expédition (remplace `useWeighing` + `useWeighingShipment`).
|
||||||
|
- Configs workflow dans `config/reception.config.ts` et `config/shipment.config.ts` (étapes, labels pesée, filename PDF).
|
||||||
|
- `WorkflowWeight` composant partagé pour les étapes de pesée (remplace `reception-weight.vue` et `shipment-weight.vue`).
|
||||||
|
- `WorkflowWaitingList` composant partagé pour les listes en attente, avec support colonnes dynamiques, slot `actions`, et prop `showActions`.
|
||||||
|
|
||||||
|
## Domaine métier clé
|
||||||
|
|
||||||
|
### Réception (pesée pont-bascule)
|
||||||
|
- Entité principale `Reception` : `date_reception` (DateTimeImmutable, format lecture `Y-m-d H:i`, écriture `Y-m-d`), `identification_number` (auto `N-BR-####`), `current_step` (défaut 0), `is_valid` (défaut false).
|
||||||
|
- `Weight` (1-N avec Reception, cascade remove + orphanRemoval) : `type` (`gross`/`tare`), `dsd`, `weight`, `weighed_at`.
|
||||||
|
- Endpoint pesée : `/receptions/weigh` → `PontBasculeReading` (dsd, weight, weighedAt).
|
||||||
|
- Endpoint suppression : `DELETE /receptions/{id}` — supprime en cascade weights, pelletBuildings, bovines.
|
||||||
|
- Parsing payload pont-bascule : `Service/PontBasculePayloadDecoder.php`.
|
||||||
|
- Exception : `PontBasculeException` (messages en français, mappée 500).
|
||||||
|
- Store Pinia `reception.ts` = source de vérité pour la réception en cours.
|
||||||
|
- UI multi-étapes dans `pages/reception/[[id]].vue` basée sur `currentStep`.
|
||||||
|
- `PrePersist` : injecte l'heure courante sur `receptionDate` à la création ; `setReceptionDate` préserve l'heure existante au PATCH.
|
||||||
|
|
||||||
|
### Expédition
|
||||||
|
- Entité `Shipment` : même pattern que Reception, `shipment_date` (format lecture `Y-m-d H:i`, écriture `Y-m-d`).
|
||||||
|
- Endpoint suppression : `DELETE /shipments/{id}`.
|
||||||
|
- `PrePersist` : injecte l'heure courante sur `shipmentDate` ; `setShipmentDate` préserve l'heure au PATCH.
|
||||||
|
|
||||||
|
### LIOT (transport)
|
||||||
|
- Si carrier code = `LIOT` : afficher sélecteurs driver + vehicle, masquer saisie plaque manuelle.
|
||||||
|
- Liste véhicules filtrée par type de camion et transporteur.
|
||||||
|
- Le véhicule sélectionné alimente `license_plate`.
|
||||||
|
- Logique mutualisée dans `composables/useLiotHandling.ts`.
|
||||||
|
|
||||||
|
### Bovins & infrastructure
|
||||||
|
- `Bovine` : `nationalNumber` (unique), `receivedWeight`, `arrivalDate`, `buildingCase` (ManyToOne).
|
||||||
|
- `BuildingCase` a `bovines` (OneToMany).
|
||||||
|
- Rapport PDF cases : `GET /building_cases/{id}/weights-report` → template Twig, projection depuis `arrivalDate`, gain journalier fixe `1.3 kg/jour`.
|
||||||
|
|
||||||
|
### Données de référence
|
||||||
|
- `ReceptionType`, `MerchandiseType`, `PelletType`, `Building`, `Supplier` (avec `Address` via join table, `createdBy` → User), `Customer` (avec `Address` via join table, `createdBy` → User), `Truck`, `Carrier`, `Driver`, `Vehicle`.
|
||||||
|
- `Address` : champ `label` nullable (déprécié, retiré du front et du `address:write`), expose `fullAddress` via getter. `countryCode` par défaut `FR` côté front.
|
||||||
|
|
||||||
|
### Seed & fixtures
|
||||||
|
- Commande `app:seed` : seed infrastructure (statut, building_layout, building_case, building_case_position) puis bovins.
|
||||||
|
- Utilise des flush intermédiaires pour que les queries find fonctionnent sur les records fraîchement créés.
|
||||||
|
- `upsertAddress` cherche par `street|postalCode` (plus par `label`).
|
||||||
|
- Fixtures : `BuildingInfrastructureFixtures` + `BovineFixtures` (via dépendances `AppFixtures`).
|
||||||
|
|
||||||
|
## Environnement
|
||||||
|
|
||||||
|
- API base dev : `http://localhost:8080/api` (via `NUXT_PUBLIC_API_BASE` dans `frontend/.env`)
|
||||||
|
- CORS : Nelmio, configurable via `CORS_ALLOW_ORIGIN` dans `.env`
|
||||||
|
- Locale par défaut : `fr` — traductions dans `frontend/i18n/locales/fr.json`
|
||||||
|
- Docker env : `docker/.env.docker` (défaut) avec override possible via `docker/.env.docker.local`
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.0.66'
|
app.version: '0.0.75'
|
||||||
|
|||||||
@@ -7,3 +7,17 @@
|
|||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.submitted :invalid {
|
||||||
|
@apply border-red-500 text-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitted :has(:invalid) > label {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitted label:has(:invalid) {
|
||||||
|
@apply text-red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validateForm">
|
<form :class="{ submitted }" @submit.prevent="validateForm">
|
||||||
<div class="flex items-center mb-11 justify-between relative">
|
<div class="flex items-center mb-11 justify-between relative">
|
||||||
<div class="flex flex-row absolute -left-[60px] ">
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
<Icon @click="goBack" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
<Icon @click="goBack" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
@@ -10,11 +10,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-y-16 gap-x-[200px] mb-16">
|
<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" required />
|
||||||
<UiTextInput id="address-street" v-model="form.street" label="Rue" />
|
|
||||||
<UiTextInput id="address-street2" v-model="form.street2" label="Complément" />
|
<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-postalCode" v-model="form.postalCode" label="Code postal" required />
|
||||||
<UiTextInput id="address-city" v-model="form.city" label="Ville" />
|
<UiSelect
|
||||||
|
id="address-city"
|
||||||
|
v-model="form.city"
|
||||||
|
label="Ville"
|
||||||
|
:options="communeOptions"
|
||||||
|
:loading="isLoadingCities"
|
||||||
|
:disabled="communes.length === 0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<UiTextInput id="address-country" v-model="form.countryCode" label="Pays (code)" />
|
<UiTextInput id="address-country" v-model="form.countryCode" label="Pays (code)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
@@ -22,6 +29,7 @@
|
|||||||
class="inline-flex items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
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"
|
type="submit"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
@click="submitted = true"
|
||||||
>
|
>
|
||||||
Valider
|
Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
@@ -31,7 +39,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AddressPayload } from "~/services/address"
|
import type { AddressPayload } from "~/services/address"
|
||||||
|
import { getCommunesByPostalCode, type CommuneData } from "~/services/geo"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -42,14 +50,20 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
const communes = ref<CommuneData[]>([])
|
||||||
|
const isLoadingCities = ref(false)
|
||||||
|
|
||||||
|
const communeOptions = computed(() =>
|
||||||
|
communes.value.map(c => ({ value: c.nom, label: c.nom }))
|
||||||
|
)
|
||||||
|
|
||||||
const emptyForm = (): AddressPayload => ({
|
const emptyForm = (): AddressPayload => ({
|
||||||
label: "",
|
|
||||||
street: "",
|
street: "",
|
||||||
street2: null,
|
street2: null,
|
||||||
postalCode: "",
|
postalCode: "",
|
||||||
city: "",
|
city: "",
|
||||||
countryCode: "",
|
countryCode: "FR",
|
||||||
})
|
})
|
||||||
|
|
||||||
const form = reactive<AddressPayload>(emptyForm())
|
const form = reactive<AddressPayload>(emptyForm())
|
||||||
@@ -70,12 +84,11 @@ const backPath = computed(() => {
|
|||||||
|
|
||||||
const hydrateForm = (address?: AddressPayload | null) => {
|
const hydrateForm = (address?: AddressPayload | null) => {
|
||||||
const data = address ?? emptyForm()
|
const data = address ?? emptyForm()
|
||||||
form.label = data.label ?? ""
|
|
||||||
form.street = data.street ?? ""
|
form.street = data.street ?? ""
|
||||||
form.street2 = data.street2 ?? null
|
form.street2 = data.street2 ?? null
|
||||||
form.postalCode = data.postalCode ?? ""
|
form.postalCode = data.postalCode ?? ""
|
||||||
form.city = data.city ?? ""
|
form.city = data.city ?? ""
|
||||||
form.countryCode = data.countryCode ?? ""
|
form.countryCode = data.countryCode || "FR"
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -86,6 +99,41 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => form.postalCode,
|
||||||
|
(cp) => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
|
||||||
|
if (!cp || cp.length < 5) {
|
||||||
|
communes.value = []
|
||||||
|
form.city = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp.length === 5) {
|
||||||
|
debounceTimer = setTimeout(async () => {
|
||||||
|
isLoadingCities.value = true
|
||||||
|
const previousCity = form.city
|
||||||
|
try {
|
||||||
|
communes.value = await getCommunesByPostalCode(cp)
|
||||||
|
|
||||||
|
if (communes.value.length === 1) {
|
||||||
|
form.city = communes.value[0].nom
|
||||||
|
} else if (communes.value.some(c => c.nom === previousCity)) {
|
||||||
|
form.city = previousCity
|
||||||
|
} else {
|
||||||
|
form.city = ''
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoadingCities.value = false
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
if (isLoading.value) return
|
if (isLoading.value) return
|
||||||
emit("validate", {...form})
|
emit("validate", {...form})
|
||||||
|
|||||||
@@ -8,14 +8,15 @@
|
|||||||
v-model="localWeight.weight"
|
v-model="localWeight.weight"
|
||||||
:disabled="!isAdmin"
|
:disabled="!isAdmin"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="48000"
|
|
||||||
wrapper-class="flex-col"
|
wrapper-class="flex-col"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiDateInput
|
<UiDateInput
|
||||||
label="Date de pesée"
|
label="Date de pesée"
|
||||||
v-model="localWeight.weighedAt"
|
v-model="localWeight.weighedAt"
|
||||||
:disabled="!isAdmin"
|
:disabled="!isAdmin"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
v-model="localWeight.dsd"
|
v-model="localWeight.dsd"
|
||||||
:disabled="!isAdmin"
|
:disabled="!isAdmin"
|
||||||
wrapper-class="flex-col"
|
wrapper-class="flex-col"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<form
|
||||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
||||||
class="flex flex-col gap-16">
|
class="flex flex-col gap-16"
|
||||||
|
@submit.prevent="goNext"
|
||||||
|
>
|
||||||
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
|
<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 w-full">
|
class="flex flex-row gap-8 items-center w-full">
|
||||||
@@ -31,15 +33,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-red-500 text-sm" :class="showBovineError ? '' : 'invisible'">
|
||||||
|
Veuillez saisir au moins une race bovine.
|
||||||
|
</p>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UiButton
|
<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"
|
||||||
@click="goNext"
|
|
||||||
>Valider
|
>Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {BovineTypeData} from "~/services/dto/bovine-type-data";
|
import type {BovineTypeData} from "~/services/dto/bovine-type-data";
|
||||||
@@ -58,6 +62,7 @@ const toast = useToast()
|
|||||||
const isLoadingBovineType = ref(false)
|
const isLoadingBovineType = ref(false)
|
||||||
const bovineType = ref<BovineTypeData[]>([])
|
const bovineType = ref<BovineTypeData[]>([])
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
|
const showBovineError = ref(false)
|
||||||
const bovineQuantities = reactive<Record<string, number | null>>({})
|
const bovineQuantities = reactive<Record<string, number | null>>({})
|
||||||
const otherQuantity = ref<number | null>(0)
|
const otherQuantity = ref<number | null>(0)
|
||||||
const receptionId = computed(() => receptionStore.current?.id ?? null)
|
const receptionId = computed(() => receptionStore.current?.id ?? null)
|
||||||
@@ -169,7 +174,13 @@ async function goNext() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO Ajouter un composable pour le toaster qui gère les key i18n
|
showBovineError.value = false
|
||||||
|
|
||||||
|
if (totalBovines.value === 0) {
|
||||||
|
showBovineError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (totalBovines.value > 52) {
|
if (totalBovines.value > 52) {
|
||||||
toast.error({
|
toast.error({
|
||||||
title: 'Erreur',
|
title: 'Erreur',
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @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 text-primary-500">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 -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-user"
|
id="reception-user"
|
||||||
v-model="form.userId"
|
v-model="form.userId"
|
||||||
@@ -13,15 +12,15 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingUsers"
|
:loading="isLoadingUsers"
|
||||||
wrapper-class="col-start-1 row-start-2"
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Date de réception -->
|
|
||||||
<UiDateInput
|
<UiDateInput
|
||||||
id="reception-date"
|
id="reception-date"
|
||||||
v-model="form.receptionDate"
|
v-model="form.receptionDate"
|
||||||
label="Date de réception"
|
label="Date de réception"
|
||||||
wrapper-class="col-start-1 row-start-3"
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Type de réception -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-type"
|
id="reception-type"
|
||||||
v-model="form.receptionTypeId"
|
v-model="form.receptionTypeId"
|
||||||
@@ -31,8 +30,8 @@
|
|||||||
label: type.label
|
label: type.label
|
||||||
}))"
|
}))"
|
||||||
wrapper-class="col-start-1 row-start-4"
|
wrapper-class="col-start-1 row-start-4"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Fournisseur -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-supplier"
|
id="reception-supplier"
|
||||||
v-model="form.supplierId"
|
v-model="form.supplierId"
|
||||||
@@ -43,20 +42,17 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingSuppliers"
|
:loading="isLoadingSuppliers"
|
||||||
wrapper-class="col-start-1 row-start-5"
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Adresse fournisseur -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-address"
|
id="reception-address"
|
||||||
v-model="form.addressId"
|
v-model="form.addressId"
|
||||||
label="Adresse"
|
label="Adresse"
|
||||||
:options="supplierAddresses.map((address) => ({
|
:options="addressOptions"
|
||||||
value: String(address.id),
|
:disabled="isLoadingSuppliers || ownerAddresses.length === 0"
|
||||||
label: address.fullAddress
|
|
||||||
}))"
|
|
||||||
:disabled="isLoadingSuppliers || supplierAddresses.length === 0"
|
|
||||||
wrapper-class="col-start-2 row-start-1"
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Camion -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-truck"
|
id="reception-truck"
|
||||||
v-model="form.truckId"
|
v-model="form.truckId"
|
||||||
@@ -67,8 +63,8 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingTrucks"
|
:loading="isLoadingTrucks"
|
||||||
wrapper-class="col-start-2 row-start-2"
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Transporteur -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-carrier"
|
id="reception-carrier"
|
||||||
v-model="form.carrierId"
|
v-model="form.carrierId"
|
||||||
@@ -80,15 +76,15 @@
|
|||||||
:loading="isLoadingCarriers"
|
:loading="isLoadingCarriers"
|
||||||
select-class="h-[34px]"
|
select-class="h-[34px]"
|
||||||
wrapper-class="col-start-2 row-start-3"
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Plaque d'immatriculation -->
|
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Immatriculation (LIOT) -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
v-if="isLiotCarrier"
|
v-if="isLiotCarrier"
|
||||||
id="reception-vehicle"
|
id="reception-vehicle"
|
||||||
@@ -101,8 +97,8 @@
|
|||||||
:loading="isLoadingVehicles"
|
:loading="isLoadingVehicles"
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
wrapper-class="col-start-2 row-start-4 h-[64px]"
|
wrapper-class="col-start-2 row-start-4 h-[64px]"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Chauffeur (LIOT) -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-driver"
|
id="reception-driver"
|
||||||
v-model="form.driverId"
|
v-model="form.driverId"
|
||||||
@@ -114,42 +110,38 @@
|
|||||||
:loading="isLoadingDrivers"
|
:loading="isLoadingDrivers"
|
||||||
v-if="isLiotCarrier"
|
v-if="isLiotCarrier"
|
||||||
wrapper-class="col-start-2 row-start-5"
|
wrapper-class="col-start-2 row-start-5"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UiButton
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
>Valider
|
>Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useReceptionStore} from '~/stores/reception'
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
import type {ReceptionTypeData} from '~/services/dto/reception-type-data'
|
import { useFormDataLoading } from '~/composables/useFormDataLoading'
|
||||||
import {getReceptionTypeList} from '~/services/reception-type'
|
import { useLiotHandling } from '~/composables/useLiotHandling'
|
||||||
import type {UserData} from '~/services/dto/user-data'
|
import { useAddressSync } from '~/composables/useAddressSync'
|
||||||
import {getUsers} from '~/services/auth'
|
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||||
import {useAuthStore} from '~/stores/auth'
|
import { getReceptionTypeList } from '~/services/reception-type'
|
||||||
import type {SupplierData} from '~/services/dto/supplier-data'
|
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||||
import {getSupplierList} from '~/services/supplier'
|
import { getSupplierList } from '~/services/supplier'
|
||||||
import type {TruckData} from '~/services/dto/truck-data'
|
import { RECEPTION_TYPE_CODES } from '~/utils/constants'
|
||||||
import {getTruckList} from '~/services/truck'
|
import { deleteReceptionBovine, getReceptionBovineList } from '~/services/reception-bovine'
|
||||||
import type {CarrierData} from '~/services/dto/carrier-data'
|
import type { ReceptionFormData } from '~/services/dto/reception-data'
|
||||||
import {getCarrierList} from '~/services/carrier'
|
|
||||||
import type {DriverData} from '~/services/dto/driver-data'
|
|
||||||
import {getDriverList} from '~/services/driver'
|
|
||||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
|
||||||
import {getVehicleList} from '~/services/vehicle'
|
|
||||||
import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
|
|
||||||
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
|
||||||
import type {ReceptionFormData} from "~/services/dto/reception-data";
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
const form = reactive<ReceptionFormData>({
|
const form = reactive<ReceptionFormData>({
|
||||||
licensePlate: '',
|
licensePlate: '',
|
||||||
receptionDate: new Date().toISOString().slice(0, 10),
|
receptionDate: new Date().toISOString().slice(0, 10),
|
||||||
@@ -162,62 +154,27 @@ const form = reactive<ReceptionFormData>({
|
|||||||
driverId: '',
|
driverId: '',
|
||||||
vehicleId: ''
|
vehicleId: ''
|
||||||
})
|
})
|
||||||
const allowAnyLicensePlate = ref(false)
|
|
||||||
const receptionTypes = ref<ReceptionTypeData[]>([])
|
const receptionTypes = ref<ReceptionTypeData[]>([])
|
||||||
const users = ref<UserData[]>([])
|
|
||||||
const isLoadingUsers = ref(false)
|
|
||||||
const suppliers = ref<SupplierData[]>([])
|
const suppliers = ref<SupplierData[]>([])
|
||||||
const isLoadingSuppliers = ref(false)
|
const isLoadingSuppliers = ref(false)
|
||||||
const trucks = ref<TruckData[]>([])
|
|
||||||
const isLoadingTrucks = ref(false)
|
|
||||||
const carriers = ref<CarrierData[]>([])
|
|
||||||
const isLoadingCarriers = ref(false)
|
|
||||||
const drivers = ref<DriverData[]>([])
|
|
||||||
const isLoadingDrivers = ref(false)
|
|
||||||
const vehicles = ref<VehicleData[]>([])
|
|
||||||
const isLoadingVehicles = ref(false)
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
// Empêche les watchers de reset des champs pendant le remplissage initial
|
|
||||||
const isHydrating = ref(false)
|
|
||||||
|
|
||||||
// Transporteur sélectionné dans le formulaire
|
const { users, trucks, carriers, isLoadingUsers, isLoadingTrucks, isLoadingCarriers, loadCommonData } =
|
||||||
const selectedCarrier = computed(() =>
|
useFormDataLoading(form)
|
||||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
|
||||||
)
|
const {
|
||||||
// Indique si le transporteur est LIOT
|
isLiotCarrier, filteredDrivers, filteredVehicles,
|
||||||
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
isLoadingDrivers, isLoadingVehicles, allowAnyLicensePlate,
|
||||||
// Adresses disponibles pour le fournisseur sélectionné
|
loadDrivers, loadVehicles
|
||||||
const supplierAddresses = computed(() => {
|
} = useLiotHandling(form, carriers, isHydrating)
|
||||||
const supplierId = Number(form.supplierId)
|
|
||||||
if (!Number.isFinite(supplierId)) {
|
const supplierIdRef = computed(() => form.supplierId)
|
||||||
return []
|
const { ownerAddresses, addressOptions } = useAddressSync(form, supplierIdRef, suppliers)
|
||||||
}
|
|
||||||
return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? []
|
|
||||||
})
|
|
||||||
// Chauffeurs filtrés par transporteur (LIOT)
|
|
||||||
const filteredDrivers = computed<DriverData[]>(() => {
|
|
||||||
if (!form.carrierId) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
|
||||||
})
|
|
||||||
// Véhicules filtrés par transporteur + type de camion
|
|
||||||
const filteredVehicles = computed<VehicleData[]>(() => {
|
|
||||||
if (!form.carrierId) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return vehicles.value.filter(
|
|
||||||
(vehicle) =>
|
|
||||||
String(vehicle.carrier?.id) === form.carrierId &&
|
|
||||||
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedReceptionType = computed(() =>
|
const selectedReceptionType = computed(() =>
|
||||||
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
|
||||||
)
|
)
|
||||||
|
|
||||||
// Supprime les données bovines si on change de type de réception
|
|
||||||
const clearReceptionBovines = async (receptionIri: string) => {
|
const clearReceptionBovines = async (receptionIri: string) => {
|
||||||
const existing = await getReceptionBovineList(receptionIri)
|
const existing = await getReceptionBovineList(receptionIri)
|
||||||
for (const selection of existing) {
|
for (const selection of existing) {
|
||||||
@@ -225,50 +182,6 @@ const clearReceptionBovines = async (receptionIri: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hydrate le formulaire depuis la réception en cours
|
|
||||||
watch(
|
|
||||||
() => receptionStore.current,
|
|
||||||
(reception) => {
|
|
||||||
isHydrating.value = true
|
|
||||||
form.licensePlate = reception?.licensePlate ?? ''
|
|
||||||
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
|
|
||||||
form.receptionTypeId = reception?.receptionType?.id
|
|
||||||
? String(reception.receptionType.id)
|
|
||||||
: ''
|
|
||||||
form.userId = reception?.user?.id
|
|
||||||
? String(reception.user.id)
|
|
||||||
: form.userId
|
|
||||||
form.supplierId = reception?.supplier?.id
|
|
||||||
? String(reception.supplier.id)
|
|
||||||
: ''
|
|
||||||
form.addressId = reception?.address?.id
|
|
||||||
? String(reception.address.id)
|
|
||||||
: ''
|
|
||||||
form.truckId = reception?.truck?.id
|
|
||||||
? String(reception.truck.id)
|
|
||||||
: ''
|
|
||||||
form.carrierId = reception?.carrier?.id
|
|
||||||
? String(reception.carrier.id)
|
|
||||||
: ''
|
|
||||||
form.driverId = reception?.driver?.id
|
|
||||||
? String(reception.driver.id)
|
|
||||||
: ''
|
|
||||||
isHydrating.value = false
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Charge la liste des users pour le select
|
|
||||||
const loadUsers = async () => {
|
|
||||||
isLoadingUsers.value = true
|
|
||||||
try {
|
|
||||||
users.value = await getUsers()
|
|
||||||
} finally {
|
|
||||||
isLoadingUsers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charge la liste des fournisseurs pour le select
|
|
||||||
const loadSuppliers = async () => {
|
const loadSuppliers = async () => {
|
||||||
isLoadingSuppliers.value = true
|
isLoadingSuppliers.value = true
|
||||||
try {
|
try {
|
||||||
@@ -278,185 +191,32 @@ const loadSuppliers = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charge la liste des camions pour le select
|
watch(
|
||||||
const loadTrucks = async () => {
|
() => receptionStore.current,
|
||||||
isLoadingTrucks.value = true
|
(reception) => {
|
||||||
try {
|
isHydrating.value = true
|
||||||
trucks.value = await getTruckList()
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
} finally {
|
form.receptionDate = reception?.receptionDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
|
||||||
isLoadingTrucks.value = false
|
form.receptionTypeId = reception?.receptionType?.id ? String(reception.receptionType.id) : ''
|
||||||
}
|
form.userId = reception?.user?.id ? String(reception.user.id) : form.userId
|
||||||
}
|
form.supplierId = reception?.supplier?.id ? String(reception.supplier.id) : ''
|
||||||
|
form.addressId = reception?.address?.id ? String(reception.address.id) : ''
|
||||||
|
form.truckId = reception?.truck?.id ? String(reception.truck.id) : ''
|
||||||
|
form.carrierId = reception?.carrier?.id ? String(reception.carrier.id) : ''
|
||||||
|
form.driverId = reception?.driver?.id ? String(reception.driver.id) : ''
|
||||||
|
isHydrating.value = false
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// Charge la liste des transporteurs pour le select
|
|
||||||
const loadCarriers = async () => {
|
|
||||||
isLoadingCarriers.value = true
|
|
||||||
try {
|
|
||||||
carriers.value = await getCarrierList()
|
|
||||||
} finally {
|
|
||||||
isLoadingCarriers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charge la liste des chauffeurs pour le select
|
|
||||||
const loadDrivers = async () => {
|
|
||||||
isLoadingDrivers.value = true
|
|
||||||
try {
|
|
||||||
drivers.value = await getDriverList()
|
|
||||||
} finally {
|
|
||||||
isLoadingDrivers.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charge la liste des véhicules pour le select
|
|
||||||
const loadVehicles = async () => {
|
|
||||||
isLoadingVehicles.value = true
|
|
||||||
try {
|
|
||||||
vehicles.value = await getVehicleList()
|
|
||||||
} finally {
|
|
||||||
isLoadingVehicles.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On met le user connecté par défaut dans le select
|
|
||||||
const setDefaultUser = () => {
|
|
||||||
if (form.userId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (authStore.user?.id) {
|
|
||||||
form.userId = String(authStore.user.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On récupère toutes les données des selects au chargement du composant
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
receptionTypes.value = await getReceptionTypeList()
|
receptionTypes.value = await getReceptionTypeList()
|
||||||
await loadUsers()
|
|
||||||
await loadSuppliers()
|
await loadSuppliers()
|
||||||
await loadTrucks()
|
await loadCommonData()
|
||||||
await loadCarriers()
|
|
||||||
await loadDrivers()
|
await loadDrivers()
|
||||||
await loadVehicles()
|
await loadVehicles()
|
||||||
await authStore.ensureSession()
|
|
||||||
setDefaultUser()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
|
||||||
watch(
|
|
||||||
() => [form.supplierId, form.addressId, suppliers.value],
|
|
||||||
() => {
|
|
||||||
if (!form.supplierId) {
|
|
||||||
form.addressId = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.addressId && supplierAddresses.value.length === 1) {
|
|
||||||
form.addressId = String(supplierAddresses.value[0].id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.addressId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const matches = supplierAddresses.value.some(
|
|
||||||
(address) => String(address.id) === form.addressId
|
|
||||||
)
|
|
||||||
if (!matches) {
|
|
||||||
if (supplierAddresses.value.length === 1) {
|
|
||||||
form.addressId = String(supplierAddresses.value[0].id)
|
|
||||||
} else {
|
|
||||||
form.addressId = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
|
||||||
watch(
|
|
||||||
() => form.carrierId,
|
|
||||||
() => {
|
|
||||||
if (isHydrating.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.carrierId) {
|
|
||||||
form.driverId = ''
|
|
||||||
form.vehicleId = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!isLiotCarrier.value) {
|
|
||||||
form.driverId = ''
|
|
||||||
form.vehicleId = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (filteredDrivers.value.length === 1) {
|
|
||||||
form.driverId = String(filteredDrivers.value[0].id)
|
|
||||||
}
|
|
||||||
if (filteredVehicles.value.length === 1) {
|
|
||||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
|
||||||
watch(
|
|
||||||
() => [form.truckId, form.carrierId, vehicles.value],
|
|
||||||
() => {
|
|
||||||
if (!isLiotCarrier.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (filteredVehicles.value.length === 1) {
|
|
||||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.vehicleId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const matches = filteredVehicles.value.some(
|
|
||||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
|
||||||
)
|
|
||||||
if (!matches) {
|
|
||||||
form.vehicleId = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
|
||||||
watch(
|
|
||||||
() => [form.vehicleId, form.carrierId, vehicles.value],
|
|
||||||
() => {
|
|
||||||
if (!isLiotCarrier.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isHydrating.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const selected = filteredVehicles.value.find(
|
|
||||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
|
||||||
)
|
|
||||||
if (selected) {
|
|
||||||
form.licensePlate = selected.plate
|
|
||||||
allowAnyLicensePlate.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [form.licensePlate, form.carrierId, vehicles.value],
|
|
||||||
() => {
|
|
||||||
if (!isLiotCarrier.value || form.vehicleId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const match = filteredVehicles.value.find(
|
|
||||||
(vehicle) => vehicle.plate === form.licensePlate
|
|
||||||
)
|
|
||||||
if (match) {
|
|
||||||
form.vehicleId = String(match.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Valide le formulaire et crée/met à jour la réception
|
|
||||||
async function validate() {
|
async function validate() {
|
||||||
const normalizedLicensePlate = form.licensePlate.trim()
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
const normalizedReceptionDate = form.receptionDate.trim()
|
const normalizedReceptionDate = form.receptionDate.trim()
|
||||||
@@ -467,27 +227,14 @@ async function validate() {
|
|||||||
const normalizedTruckId = form.truckId.trim()
|
const normalizedTruckId = form.truckId.trim()
|
||||||
const normalizedCarrierId = form.carrierId.trim()
|
const normalizedCarrierId = form.carrierId.trim()
|
||||||
const normalizedDriverId = form.driverId.trim()
|
const normalizedDriverId = form.driverId.trim()
|
||||||
const receptionTypeIri = normalizedReceptionTypeId
|
|
||||||
? `/api/reception_types/${normalizedReceptionTypeId}`
|
const receptionTypeIri = normalizedReceptionTypeId ? `/api/reception_types/${normalizedReceptionTypeId}` : null
|
||||||
: null
|
const userIri = normalizedUserId ? `/api/users/${normalizedUserId}` : null
|
||||||
const userIri = normalizedUserId
|
const supplierIri = normalizedSupplierId ? `/api/suppliers/${normalizedSupplierId}` : null
|
||||||
? `/api/users/${normalizedUserId}`
|
const addressIri = normalizedAddressId ? `/api/addresses/${normalizedAddressId}` : null
|
||||||
: null
|
const truckIri = normalizedTruckId ? `/api/trucks/${normalizedTruckId}` : null
|
||||||
const supplierIri = normalizedSupplierId
|
const carrierIri = normalizedCarrierId ? `/api/carriers/${normalizedCarrierId}` : null
|
||||||
? `/api/suppliers/${normalizedSupplierId}`
|
const driverIri = normalizedDriverId ? `/api/drivers/${normalizedDriverId}` : null
|
||||||
: null
|
|
||||||
const addressIri = normalizedAddressId
|
|
||||||
? `/api/addresses/${normalizedAddressId}`
|
|
||||||
: null
|
|
||||||
const truckIri = normalizedTruckId
|
|
||||||
? `/api/trucks/${normalizedTruckId}`
|
|
||||||
: null
|
|
||||||
const carrierIri = normalizedCarrierId
|
|
||||||
? `/api/carriers/${normalizedCarrierId}`
|
|
||||||
: null
|
|
||||||
const driverIri = normalizedDriverId
|
|
||||||
? `/api/drivers/${normalizedDriverId}`
|
|
||||||
: null
|
|
||||||
|
|
||||||
const basePayload = {
|
const basePayload = {
|
||||||
licensePlate: normalizedLicensePlate,
|
licensePlate: normalizedLicensePlate,
|
||||||
@@ -502,7 +249,7 @@ async function validate() {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...basePayload,
|
...basePayload,
|
||||||
...(isLiotCarrier.value && driverIri ? {driver: driverIri} : {})
|
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!receptionStore.current) {
|
if (!receptionStore.current) {
|
||||||
@@ -532,5 +279,4 @@ async function validate() {
|
|||||||
...payload
|
...payload
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center gap-16">
|
<form :class="['flex flex-col items-center gap-16', { submitted }]" @submit.prevent="goNext">
|
||||||
<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">
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
label="Type de marchandises"
|
label="Type de marchandises"
|
||||||
:options="merchandiseTypes.map((type) => ({ value: String(type.id), label: type.label }))"
|
:options="merchandiseTypes.map((type) => ({ value: String(type.id), label: type.label }))"
|
||||||
wrapper-class="w-[550px]"
|
wrapper-class="w-[550px]"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="selectedMerchandiseTypeId && isAutres"
|
v-if="selectedMerchandiseTypeId && isAutres"
|
||||||
@@ -21,24 +22,30 @@
|
|||||||
label="Préciser"
|
label="Préciser"
|
||||||
placeholder="Précisions complémentaires"
|
placeholder="Précisions complémentaires"
|
||||||
:maxlength="255"
|
:maxlength="255"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
class="flex gap-4 w-[550px] justify-between"
|
class="flex flex-col gap-4 w-[550px]"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex gap-4 justify-between">
|
||||||
v-for="building in buildings"
|
<div
|
||||||
:key="building.id"
|
v-for="building in buildings"
|
||||||
>
|
:key="building.id"
|
||||||
<UiCheckbox
|
>
|
||||||
v-model="selectedBuildingIds"
|
<UiCheckbox
|
||||||
:value="String(building.id)"
|
v-model="selectedBuildingIds"
|
||||||
:label="building.label"
|
:value="String(building.id)"
|
||||||
label-class="text-xl"
|
:label="building.label"
|
||||||
/>
|
label-class="text-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-red-500 text-sm" :class="showBuildingError ? '' : 'invisible'">
|
||||||
|
Veuillez sélectionner au moins un bâtiment.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -62,17 +69,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-red-500 text-sm" :class="showBuildingError ? '' : 'invisible'">
|
||||||
|
Veuillez sélectionner au moins un bâtiment.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UiButton
|
<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"
|
||||||
@click="goNext"
|
@click="submitted = true"
|
||||||
>Valider
|
>Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -100,6 +110,9 @@ const selectedMerchandiseTypeId = ref('')
|
|||||||
const selectedBuildingIds = ref<string[]>([])
|
const selectedBuildingIds = ref<string[]>([])
|
||||||
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||||
const merchandiseDetail = ref('')
|
const merchandiseDetail = ref('')
|
||||||
|
const submitted = ref(false)
|
||||||
|
const showBuildingError = ref(false)
|
||||||
|
const showPelletBuildingError = ref(false)
|
||||||
|
|
||||||
// Extrait l'ID d'une relation depuis un IRI ou un objet complet.
|
// Extrait l'ID d'une relation depuis un IRI ou un objet complet.
|
||||||
const getRelationId = (value: unknown): string | null => {
|
const getRelationId = (value: unknown): string | null => {
|
||||||
@@ -182,6 +195,23 @@ async function goNext() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showBuildingError.value = false
|
||||||
|
showPelletBuildingError.value = false
|
||||||
|
|
||||||
|
if (!isGranule.value && !isAutres.value && selectedBuildingIds.value.length === 0) {
|
||||||
|
showBuildingError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGranule.value) {
|
||||||
|
const hasAnyPelletBuilding = Object.values(selectedPelletBuildingIds.value)
|
||||||
|
.some((ids) => ids.length > 0)
|
||||||
|
if (!hasAnyPelletBuilding) {
|
||||||
|
showPelletBuildingError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nextStep = receptionStore.current.currentStep + 1
|
const nextStep = receptionStore.current.currentStep + 1
|
||||||
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
||||||
|
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
|
||||||
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
|
||||||
<!--@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 text-primary-500 mt-2">Pont-bascule connecté</p>
|
|
||||||
<div
|
|
||||||
v-if="showLoadingBox"
|
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
|
||||||
<UiLoadingDots />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="displayWeight !== null" class="w-full">
|
|
||||||
<div
|
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl 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, onMounted} from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useWeighing } from '~/composables/useWeighing'
|
|
||||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
mode: 'gross' | 'tare'
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const receptionStore = useReceptionStore()
|
|
||||||
const { current: storeReception } = storeToRefs(receptionStore)
|
|
||||||
const { printPdf } = usePdfPrinter()
|
|
||||||
const {
|
|
||||||
displayWeight,
|
|
||||||
title,
|
|
||||||
showLoadingBox,
|
|
||||||
fetchWeight,
|
|
||||||
saveWeight
|
|
||||||
} = useWeighing({
|
|
||||||
mode: props.mode,
|
|
||||||
reception: storeReception,
|
|
||||||
updateReception: receptionStore.updateReception,
|
|
||||||
loadReception: receptionStore.loadReception
|
|
||||||
})
|
|
||||||
// Affiche le bouton de génération du bon à l'étape tare
|
|
||||||
const showGenerateReceipt = computed(
|
|
||||||
() => props.mode === 'tare' && displayWeight.value !== null
|
|
||||||
)
|
|
||||||
|
|
||||||
// Génère le bon de réception, puis clôture la réception
|
|
||||||
const printReceipt = async () => {
|
|
||||||
if (!import.meta.client || !receptionStore.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveWeight()
|
|
||||||
const reception = receptionStore.current
|
|
||||||
const filename = `${reception.identificationNumber ?? reception.id}_${reception.supplier?.name ?? 'fournisseur'}_${reception.licensePlate ?? 'immat'}.pdf`
|
|
||||||
await printPdf(`/receptions/${reception.id}/receipt`, filename)
|
|
||||||
|
|
||||||
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
|
||||||
|
|
||||||
const result = await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
isValid: true
|
|
||||||
})
|
|
||||||
if (!result) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
receptionStore.clearCurrent()
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupère le poids dès l'arrivée sur l'écran
|
|
||||||
onMounted(() => {
|
|
||||||
if (displayWeight.value === null) {
|
|
||||||
fetchWeight()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form>
|
<form>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="w-full col-start-1 row-start-1">
|
<div class="w-full relative grid grid-cols-[1fr_200px]">
|
||||||
<UiRadioGroup
|
<UiRadioGroup
|
||||||
id="merchandise-type"
|
id="merchandise-type"
|
||||||
v-model="selectedMerchandiseTypeId"
|
v-model="selectedMerchandiseTypeId"
|
||||||
@@ -13,15 +13,25 @@
|
|||||||
input-class="accent-primary-700 focus:ring-primary-700"
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
option-label-class="uppercase"
|
option-label-class="uppercase"
|
||||||
wrapper-class="w-full uppercase"
|
wrapper-class="w-full uppercase"
|
||||||
group-class="grid grid-cols-[336px_336px_355px_200px] w-[160px_160px_200px_180px] mt-9 mb-7"
|
group-class="grid grid-cols-4 mt-9 mb-7"
|
||||||
:disabled="!isAdmin"
|
:disabled="!isAdmin"
|
||||||
/>
|
/>
|
||||||
|
<UiTextInput
|
||||||
|
v-if="isAutres"
|
||||||
|
id="merchandise-detail"
|
||||||
|
:disabled="!isAdmin"
|
||||||
|
v-model="merchandiseDetail"
|
||||||
|
placeholder="Préciser"
|
||||||
|
:maxlength="255"
|
||||||
|
wrapper-class="w-[200px] mt-12 mb-7"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full grid grid-cols-[3fr_1fr] gap-12 col-start-2 row-start-1">
|
<div
|
||||||
<div
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
class="w-full grid grid-cols-[1fr_200px]"
|
||||||
class="flex gap-[218px]"
|
>
|
||||||
|
<div class="grid grid-cols-4 gap-6"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="building in buildings"
|
v-for="building in buildings"
|
||||||
@@ -33,31 +43,17 @@
|
|||||||
:label="building.label"
|
:label="building.label"
|
||||||
:disabled="!isAdmin"
|
:disabled="!isAdmin"
|
||||||
input-class="accent-primary-700 focus:ring-primary-700"
|
input-class="accent-primary-700 focus:ring-primary-700"
|
||||||
label-class="text-xl uppercase"
|
label-class="uppercase"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="selectedMerchandiseTypeId && isGranule"
|
v-if="selectedMerchandiseTypeId && isGranule"
|
||||||
class="flex flex-col gap-10 w-full col-start-2 row-start-1"
|
class="grid grid-cols-[1fr_200px] 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 class="grid grid-cols-4 gap-6 justify-between">
|
||||||
<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="mb-1 font-medium uppercase">{{ type.label }}</p>
|
<p class="mb-1 font-medium uppercase">{{ type.label }}</p>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
<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>
|
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Expédition</h1>
|
||||||
<!-- Nom de l'utilisateur -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="shipment-user"
|
id="shipment-user"
|
||||||
v-model="form.userId"
|
v-model="form.userId"
|
||||||
@@ -13,15 +12,15 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingUsers"
|
:loading="isLoadingUsers"
|
||||||
wrapper-class="col-start-1 row-start-2"
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Date de l'éxpedition -->
|
|
||||||
<UiDateInput
|
<UiDateInput
|
||||||
id="shipment-date"
|
id="shipment-date"
|
||||||
v-model="form.shipmentDate"
|
v-model="form.shipmentDate"
|
||||||
label="Date du jour"
|
label="Date du jour"
|
||||||
wrapper-class="col-start-1 row-start-3"
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Type d'expédition -->
|
|
||||||
<div class="col-start-1 row-start-4 h-[64px]">
|
<div class="col-start-1 row-start-4 h-[64px]">
|
||||||
<div class="flex w-full items-end gap-[104px]">
|
<div class="flex w-full items-end gap-[104px]">
|
||||||
<UiRadioGroup
|
<UiRadioGroup
|
||||||
@@ -36,6 +35,7 @@
|
|||||||
value: String(type.id),
|
value: String(type.id),
|
||||||
label: type.label
|
label: type.label
|
||||||
}))"
|
}))"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
id="shipment-type-quantity"
|
id="shipment-type-quantity"
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Client -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="shipment-customer"
|
id="shipment-customer"
|
||||||
v-model="form.customerId"
|
v-model="form.customerId"
|
||||||
@@ -58,17 +57,17 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingCustomers"
|
:loading="isLoadingCustomers"
|
||||||
wrapper-class="col-start-1 row-start-5"
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Adresse du client -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="shipment-address"
|
id="shipment-address"
|
||||||
v-model="form.addressId"
|
v-model="form.addressId"
|
||||||
:options="customerAddressOptions"
|
:options="addressOptions"
|
||||||
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
:disabled="isLoadingCustomers || ownerAddresses.length === 0"
|
||||||
label="Adresse"
|
label="Adresse"
|
||||||
wrapper-class="col-start-2 row-start-1"
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Camion -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="shipment-truck"
|
id="shipment-truck"
|
||||||
v-model="form.truckId"
|
v-model="form.truckId"
|
||||||
@@ -79,8 +78,8 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingTrucks"
|
:loading="isLoadingTrucks"
|
||||||
wrapper-class="col-start-2 row-start-2"
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Transporteur -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="shipment-carrier"
|
id="shipment-carrier"
|
||||||
v-model="form.carrierId"
|
v-model="form.carrierId"
|
||||||
@@ -90,15 +89,15 @@
|
|||||||
label: carrier.name
|
label: carrier.name
|
||||||
}))"
|
}))"
|
||||||
wrapper-class="col-start-2 row-start-3"
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Plaque d'immatriculation (hors LIOT) -->
|
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Immatriculation (LIOT) -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
v-if="isLiotCarrier"
|
v-if="isLiotCarrier"
|
||||||
id="shipment-vehicle"
|
id="shipment-vehicle"
|
||||||
@@ -111,8 +110,8 @@
|
|||||||
:loading="isLoadingVehicles"
|
:loading="isLoadingVehicles"
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
wrapper-class="col-start-2 row-start-4"
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Chauffeur (LIOT) -->
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="shipment-driver"
|
id="shipment-driver"
|
||||||
v-model="form.driverId"
|
v-model="form.driverId"
|
||||||
@@ -124,67 +123,35 @@
|
|||||||
:loading="isLoadingDrivers"
|
:loading="isLoadingDrivers"
|
||||||
wrapper-class="col-start-2 row-start-5"
|
wrapper-class="col-start-2 row-start-5"
|
||||||
v-if="isLiotCarrier"
|
v-if="isLiotCarrier"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UiButton
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
>Valider
|
>Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useFormDataLoading } from '~/composables/useFormDataLoading'
|
||||||
|
import { useLiotHandling } from '~/composables/useLiotHandling'
|
||||||
|
import { useAddressSync } from '~/composables/useAddressSync'
|
||||||
|
import type { CustomerData } from '~/services/dto/customer-data'
|
||||||
|
import { getCustomerList } from '~/services/customer'
|
||||||
|
import type { ShipmentFormData } from '~/services/dto/shipment-data'
|
||||||
|
import { useShipmentStore } from '~/stores/shipment'
|
||||||
|
import type { ShipmentTypeData } from '~/services/dto/shipment-type-data'
|
||||||
|
import { getShipmentTypeList } from '~/services/shipment-type'
|
||||||
|
|
||||||
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 router = useRouter()
|
||||||
const bovineShipment = ref<ShipmentTypeData[]>([])
|
const shipmentStore = useShipmentStore()
|
||||||
const selectedShipmentTypeId = ref('')
|
const isHydrating = ref(false)
|
||||||
const shipmentQuantity = ref<number | null>(0)
|
const submitted = ref(false)
|
||||||
// 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>({
|
const form = reactive<ShipmentFormData>({
|
||||||
userId: '',
|
userId: '',
|
||||||
@@ -197,58 +164,24 @@ const form = reactive<ShipmentFormData>({
|
|||||||
vehicleId: '',
|
vehicleId: '',
|
||||||
licensePlate: '',
|
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 () => {
|
const customers = ref<CustomerData[]>([])
|
||||||
isLoadingShipmentTypes.value = true
|
const isLoadingCustomers = ref(false)
|
||||||
try {
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
bovineShipment.value = await getShipmentTypeList()
|
const selectedShipmentTypeId = ref('')
|
||||||
} finally {
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
isLoadingShipmentTypes.value = false
|
|
||||||
}
|
const { users, trucks, carriers, isLoadingUsers, isLoadingTrucks, isLoadingCarriers, loadCommonData } =
|
||||||
}
|
useFormDataLoading(form)
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLiotCarrier, filteredDrivers, filteredVehicles,
|
||||||
|
isLoadingDrivers, isLoadingVehicles, allowAnyLicensePlate,
|
||||||
|
loadDrivers, loadVehicles
|
||||||
|
} = useLiotHandling(form, carriers, isHydrating)
|
||||||
|
|
||||||
|
const customerIdRef = computed(() => form.customerId)
|
||||||
|
const { ownerAddresses, addressOptions } = useAddressSync(form, customerIdRef, customers)
|
||||||
|
|
||||||
const loadCustomers = async () => {
|
const loadCustomers = async () => {
|
||||||
isLoadingCustomers.value = true
|
isLoadingCustomers.value = true
|
||||||
@@ -257,212 +190,51 @@ const loadCustomers = async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
isLoadingCustomers.value = false
|
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(
|
watch(
|
||||||
() => shipmentStore.current,
|
() => shipmentStore.current,
|
||||||
(shipment) => {
|
(shipment) => {
|
||||||
isHydrating.value = true
|
isHydrating.value = true
|
||||||
form.licensePlate = shipment?.licensePlate ?? ''
|
form.licensePlate = shipment?.licensePlate ?? ''
|
||||||
form.shipmentDate = shipment?.shipmentDate ?? new Date().toISOString().slice(0, 10)
|
form.shipmentDate = shipment?.shipmentDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
|
||||||
form.userId = shipment?.user?.id ? String(shipment.user.id) :
|
form.userId = shipment?.user?.id ? String(shipment.user.id) : form.userId
|
||||||
form.userId
|
form.customerId = shipment?.customer?.id ? String(shipment.customer.id) : ''
|
||||||
form.customerId = shipment?.customer?.id ?
|
|
||||||
String(shipment.customer.id) : ''
|
|
||||||
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
|
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
|
||||||
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
|
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
|
||||||
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
|
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
|
||||||
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
|
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
|
||||||
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
|
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
|
||||||
|
selectedShipmentTypeId.value = shipment?.shipmentType?.id ? String(shipment.shipmentType.id) : ''
|
||||||
|
|
||||||
selectedShipmentTypeId.value = shipment?.shipmentType?.id
|
|
||||||
? String(shipment.shipmentType.id)
|
|
||||||
: ''
|
|
||||||
|
|
||||||
shipmentQuantity.value = shipment?.nbBovinSend ?? 0
|
shipmentQuantity.value = shipment?.nbBovinSend ?? 0
|
||||||
|
|
||||||
|
|
||||||
isHydrating.value = false
|
isHydrating.value = false
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{ 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}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Extra watcher for LIOT defaults after hydration
|
||||||
watch(
|
watch(
|
||||||
() => isHydrating.value,
|
() => isHydrating.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (!value) {
|
if (!value && isLiotCarrier.value) {
|
||||||
applyLiotDefaults()
|
if (filteredDrivers.value.length === 1 && !form.driverId) {
|
||||||
}
|
form.driverId = String(filteredDrivers.value[0].id)
|
||||||
}
|
}
|
||||||
)
|
if (filteredVehicles.value.length === 1 && !form.vehicleId) {
|
||||||
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
bovineShipment.value = await getShipmentTypeList()
|
||||||
|
await loadCustomers()
|
||||||
|
await loadCommonData()
|
||||||
|
await loadVehicles()
|
||||||
|
await loadDrivers()
|
||||||
|
})
|
||||||
|
|
||||||
const buildPayload = () => {
|
const buildPayload = () => {
|
||||||
const normalizedLicensePlate = form.licensePlate.trim()
|
const normalizedLicensePlate = form.licensePlate.trim()
|
||||||
const normalizedShipmentDate = form.shipmentDate.trim()
|
const normalizedShipmentDate = form.shipmentDate.trim()
|
||||||
@@ -472,32 +244,18 @@ const buildPayload = () => {
|
|||||||
const normalizedDriverId = form.driverId.trim()
|
const normalizedDriverId = form.driverId.trim()
|
||||||
const normalizedUserId = form.userId.trim()
|
const normalizedUserId = form.userId.trim()
|
||||||
const normalizedAddressId = form.addressId.trim()
|
const normalizedAddressId = form.addressId.trim()
|
||||||
const customerIri = normalizedCustomerId
|
|
||||||
? `/api/customers/${normalizedCustomerId}`
|
const customerIri = normalizedCustomerId ? `/api/customers/${normalizedCustomerId}` : null
|
||||||
: null
|
const truckIri = normalizedTruckId ? `/api/trucks/${normalizedTruckId}` : null
|
||||||
const truckIri = normalizedTruckId
|
const carrierIri = normalizedCarrierId ? `/api/carriers/${normalizedCarrierId}` : null
|
||||||
? `/api/trucks/${normalizedTruckId}`
|
const userIri = normalizedUserId ? `/api/users/${normalizedUserId}` : null
|
||||||
: null
|
const driverIri = normalizedDriverId ? `/api/drivers/${normalizedDriverId}` : null
|
||||||
const carrierIri = normalizedCarrierId
|
const addressIri = normalizedAddressId ? `/api/addresses/${normalizedAddressId}` : null
|
||||||
? `/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 normalizedShipmentTypeId = selectedShipmentTypeId.value.trim()
|
||||||
const shipmentTypeIri = normalizedShipmentTypeId
|
const shipmentTypeIri = normalizedShipmentTypeId ? `/api/shipment_types/${normalizedShipmentTypeId}` : null
|
||||||
? `/api/shipment_types/${normalizedShipmentTypeId}`
|
|
||||||
: null
|
|
||||||
|
|
||||||
const rawQuantity = Number(shipmentQuantity.value ?? 0)
|
const rawQuantity = Number(shipmentQuantity.value ?? 0)
|
||||||
const normalizedQuantity = Number.isFinite(rawQuantity) ? Math.max(0,
|
const normalizedQuantity = Number.isFinite(rawQuantity) ? Math.max(0, Math.trunc(rawQuantity)) : 0
|
||||||
Math.trunc(rawQuantity)) : 0
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
licensePlate: normalizedLicensePlate,
|
licensePlate: normalizedLicensePlate,
|
||||||
@@ -522,15 +280,14 @@ const saveDraft = async () => {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
currentStep: shipmentStore.current.currentStep,
|
currentStep: shipmentStore.current.currentStep,
|
||||||
...payload
|
...payload
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({saveDraft})
|
defineExpose({ saveDraft })
|
||||||
// Valide le formulaire et crée/met à jour l'expédition
|
|
||||||
const validate = async () => {
|
const validate = async () => {
|
||||||
const payload = buildPayload()
|
const payload = buildPayload()
|
||||||
if (!shipmentStore.current) {
|
if (!shipmentStore.current) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center gap-[118px]">
|
<div class="flex flex-col items-center gap-[150px]">
|
||||||
<h1 class="font-bold text-5xl uppercase text-primary-500">Chargement des bovins</h1>
|
<h1 class="font-bold text-5xl uppercase text-primary-500">Chargement des bovins</h1>
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col items-center justify-center">
|
class="w-full flex flex-col items-center justify-center">
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
|
||||||
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
|
||||||
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
|
||||||
<div
|
|
||||||
v-if="showLoadingBox"
|
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
|
||||||
<UiLoadingDots />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="displayWeight !== null" class="w-full">
|
|
||||||
<div
|
|
||||||
class="w-full flex flex-col items-center justify-center border border-primary-500 h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
|
|
||||||
{{ displayWeight }} kg
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center mt-[54px]">
|
|
||||||
<UiButton
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
|
||||||
@click="fetchWeight"
|
|
||||||
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
|
||||||
<UiButton
|
|
||||||
v-if="displayWeight !== null && !showGenerateReceipt"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
|
||||||
@click="saveWeight"
|
|
||||||
>Valider la pesée</UiButton>
|
|
||||||
<UiButton
|
|
||||||
v-if="showGenerateReceipt"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
|
||||||
@click="printReceipt"
|
|
||||||
>Générer le bon</UiButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useWeighingShipment } from '~/composables/useWeighing'
|
|
||||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
|
||||||
import { useShipmentStore } from '~/stores/shipment'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
mode: 'gross' | 'tare'
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const shipmentStore = useShipmentStore()
|
|
||||||
const { current: storeShipment } = storeToRefs(shipmentStore)
|
|
||||||
const { printPdf } = usePdfPrinter()
|
|
||||||
const {
|
|
||||||
displayWeight,
|
|
||||||
title,
|
|
||||||
showLoadingBox,
|
|
||||||
fetchWeight,
|
|
||||||
saveWeight
|
|
||||||
} = useWeighingShipment({
|
|
||||||
modeShipment: props.mode,
|
|
||||||
shipment: storeShipment,
|
|
||||||
updateShipment: shipmentStore.updateShipment,
|
|
||||||
loadShipment: shipmentStore.loadShipment
|
|
||||||
})
|
|
||||||
// Affiche le bouton de génération du bon à l'étape tare
|
|
||||||
const showGenerateReceipt = computed(
|
|
||||||
() => props.mode === 'tare' && displayWeight.value !== null
|
|
||||||
)
|
|
||||||
|
|
||||||
// Génère le bon d'expédition, puis clôture l'expédition
|
|
||||||
const printReceipt = async () => {
|
|
||||||
if (!import.meta.client || !shipmentStore.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveWeight()
|
|
||||||
const shipment = shipmentStore.current
|
|
||||||
const filename = `${shipment.identificationNumber ?? shipment.id}_${shipment.customer?.label ?? 'client'}_${shipment.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('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupère le poids dès l'arrivée sur l'écran
|
|
||||||
onMounted(() => {
|
|
||||||
if (displayWeight.value === null) {
|
|
||||||
fetchWeight()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:maxlength="maxLength"
|
:maxlength="maxLength"
|
||||||
:placeholder="placeholderText"
|
:placeholder="placeholderText"
|
||||||
|
:required="required"
|
||||||
class="border-b border-black flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
|
class="border-b border-black flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
@@ -32,12 +33,14 @@ type Props = {
|
|||||||
allowAny?: boolean
|
allowAny?: boolean
|
||||||
label?: string
|
label?: string
|
||||||
id?: string
|
id?: string
|
||||||
|
required?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
allowAny: false,
|
allowAny: false,
|
||||||
label: 'Immatriculation',
|
label: 'Immatriculation',
|
||||||
id: 'license-plate'
|
id: 'license-plate',
|
||||||
|
required: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
57
frontend/components/workflow/workflow-liot-fields.vue
Normal file
57
frontend/components/workflow/workflow-liot-fields.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="!isLiotCarrier">
|
||||||
|
<div :class="wrapperClass">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licensePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="isLiotCarrier">
|
||||||
|
<UiSelect
|
||||||
|
:id="`${idPrefix}-vehicle`"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
:wrapper-class="wrapperClass"
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
:id="`${idPrefix}-driver`"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
:wrapper-class="driverWrapperClass"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
idPrefix: string
|
||||||
|
form: { licensePlate: string; vehicleId: string; driverId: string }
|
||||||
|
isLiotCarrier: boolean
|
||||||
|
allowAnyLicensePlate: boolean
|
||||||
|
filteredVehicles: VehicleData[]
|
||||||
|
filteredDrivers: DriverData[]
|
||||||
|
isLoadingVehicles: boolean
|
||||||
|
isLoadingDrivers: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
driverWrapperClass?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
'update:allowAnyLicensePlate': [value: boolean]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
72
frontend/components/workflow/workflow-waiting-list.vue
Normal file
72
frontend/components/workflow/workflow-waiting-list.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
|
<h1 class="text-3xl font-bold uppercase text-primary-500">{{ title }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-[86px]">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16">
|
||||||
|
<div
|
||||||
|
class="grid gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
:style="{ gridTemplateColumns: gridCols }"
|
||||||
|
>
|
||||||
|
<div v-for="col in columns" :key="col.key">{{ col.label }}</div>
|
||||||
|
<div v-if="showActions">Actions</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
class="grid gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
:style="{ gridTemplateColumns: gridCols }"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToItem(item.id)"
|
||||||
|
@keydown.enter="goToItem(item.id)"
|
||||||
|
>
|
||||||
|
<div v-for="col in columns" :key="col.key">
|
||||||
|
<slot :name="`cell-${col.key}`" :item="item">
|
||||||
|
{{ getNestedValue(item, col.key) }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="showActions" @click.stop>
|
||||||
|
<slot name="actions" :item="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Column {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
title: string
|
||||||
|
columns: Column[]
|
||||||
|
items: any[]
|
||||||
|
routePrefix: string
|
||||||
|
showActions?: boolean
|
||||||
|
}>(), {
|
||||||
|
showActions: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const gridCols = computed(() => {
|
||||||
|
const dataCols = props.columns.map(() => '1fr').join(' ')
|
||||||
|
return props.showActions ? `${dataCols} 60px` : dataCols
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToItem = (id: number) => {
|
||||||
|
router.push(`${props.routePrefix}/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNestedValue = (obj: any, path: string): string => {
|
||||||
|
const value = path.split('.').reduce((acc, key) => acc?.[key], obj)
|
||||||
|
return value || '—'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
78
frontend/components/workflow/workflow-weight.vue
Normal file
78
frontend/components/workflow/workflow-weight.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
|
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
||||||
|
<div
|
||||||
|
v-if="!displayWeight"
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<div v-else class="w-full">
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
|
||||||
|
{{ displayWeight }} kg
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-[54px]">
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
@click="fetchWeight"
|
||||||
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="saveWeight"
|
||||||
|
>Valider la pesée</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="printReceipt"
|
||||||
|
>Générer le bon</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRef } from 'vue'
|
||||||
|
import { useWeighingStep } from '~/composables/steps/useWeighingStep'
|
||||||
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
entityName: 'reception' | 'shipment'
|
||||||
|
apiResource: string
|
||||||
|
titleLabel: string
|
||||||
|
isFinal: boolean
|
||||||
|
entity: any
|
||||||
|
getWeightFromScale: () => Promise<WeightData>
|
||||||
|
updateEntity: (id: number, payload: any) => Promise<any>
|
||||||
|
loadEntity: (id: number) => Promise<any>
|
||||||
|
clearEntity: () => void
|
||||||
|
buildReceiptFilename: (entity: any) => string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const entityRef = toRef(props, 'entity')
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayWeight,
|
||||||
|
title,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight,
|
||||||
|
showGenerateReceipt,
|
||||||
|
printReceipt
|
||||||
|
} = useWeighingStep({
|
||||||
|
mode: props.mode,
|
||||||
|
entity: entityRef,
|
||||||
|
entityName: props.entityName,
|
||||||
|
apiResource: props.apiResource,
|
||||||
|
titleLabel: props.titleLabel,
|
||||||
|
isFinal: props.isFinal,
|
||||||
|
getWeightFromScale: props.getWeightFromScale,
|
||||||
|
updateEntity: props.updateEntity,
|
||||||
|
loadEntity: props.loadEntity,
|
||||||
|
clearEntity: props.clearEntity,
|
||||||
|
buildReceiptFilename: props.buildReceiptFilename
|
||||||
|
})
|
||||||
|
</script>
|
||||||
77
frontend/composables/steps/useWeighingStep.ts
Normal file
77
frontend/composables/steps/useWeighingStep.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { useWeighing } from '~/composables/useWeighing'
|
||||||
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
interface UseWeighingStepOptions {
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
entity: Ref<any>
|
||||||
|
entityName: 'reception' | 'shipment'
|
||||||
|
apiResource: string
|
||||||
|
titleLabel: string
|
||||||
|
isFinal: boolean
|
||||||
|
getWeightFromScale: () => Promise<WeightData>
|
||||||
|
updateEntity: (id: number, payload: any) => Promise<any>
|
||||||
|
loadEntity: (id: number) => Promise<any>
|
||||||
|
clearEntity: () => void
|
||||||
|
buildReceiptFilename: (entity: any) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWeighingStep = (options: UseWeighingStepOptions) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
|
||||||
|
const {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
} = useWeighing({
|
||||||
|
mode: options.mode,
|
||||||
|
entity: options.entity,
|
||||||
|
entityName: options.entityName,
|
||||||
|
apiResource: options.apiResource,
|
||||||
|
titleLabel: options.titleLabel,
|
||||||
|
getWeightFromScale: options.getWeightFromScale,
|
||||||
|
updateEntity: options.updateEntity,
|
||||||
|
loadEntity: options.loadEntity
|
||||||
|
})
|
||||||
|
|
||||||
|
const showGenerateReceipt = computed(
|
||||||
|
() => options.isFinal && displayWeight.value !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
const printReceipt = async () => {
|
||||||
|
if (!import.meta.client || !options.entity.value) return
|
||||||
|
|
||||||
|
await saveWeight()
|
||||||
|
const entity = options.entity.value
|
||||||
|
const filename = options.buildReceiptFilename(entity)
|
||||||
|
await printPdf(`/${options.apiResource}/${entity.id}/receipt`, filename)
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
const result = await options.updateEntity(entity.id, { isValid: true })
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
options.clearEntity()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight,
|
||||||
|
showGenerateReceipt,
|
||||||
|
printReceipt
|
||||||
|
}
|
||||||
|
}
|
||||||
54
frontend/composables/useAddressSync.ts
Normal file
54
frontend/composables/useAddressSync.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { AddressData } from '~/services/dto/address-data'
|
||||||
|
|
||||||
|
interface AddressOwner {
|
||||||
|
id: number
|
||||||
|
addresses?: AddressData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAddressSync = (
|
||||||
|
form: { addressId: string },
|
||||||
|
ownerId: Ref<string>,
|
||||||
|
owners: Ref<AddressOwner[]>
|
||||||
|
) => {
|
||||||
|
const ownerAddresses = computed<AddressData[]>(() => {
|
||||||
|
const id = Number(ownerId.value)
|
||||||
|
if (!Number.isFinite(id)) return []
|
||||||
|
return owners.value.find((owner) => owner.id === id)?.addresses ?? []
|
||||||
|
})
|
||||||
|
|
||||||
|
const addressOptions = computed(() =>
|
||||||
|
ownerAddresses.value.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [ownerId.value, form.addressId, owners.value],
|
||||||
|
() => {
|
||||||
|
if (!ownerId.value) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && ownerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(ownerAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) return
|
||||||
|
const matches = ownerAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (ownerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(ownerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ownerAddresses, addressOptions }
|
||||||
|
}
|
||||||
73
frontend/composables/useFormDataLoading.ts
Normal file
73
frontend/composables/useFormDataLoading.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { TruckData } from '~/services/dto/truck-data'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import { getUsers } from '~/services/auth'
|
||||||
|
import { getTruckList } from '~/services/truck'
|
||||||
|
import { getCarrierList } from '~/services/carrier'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
|
export const useFormDataLoading = (form: { userId: string }) => {
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTrucks = async () => {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCarriers = async () => {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDefaultUser = () => {
|
||||||
|
if (form.userId) return
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCommonData = async () => {
|
||||||
|
await loadUsers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
trucks,
|
||||||
|
carriers,
|
||||||
|
isLoadingUsers,
|
||||||
|
isLoadingTrucks,
|
||||||
|
isLoadingCarriers,
|
||||||
|
loadUsers,
|
||||||
|
loadTrucks,
|
||||||
|
loadCarriers,
|
||||||
|
setDefaultUser,
|
||||||
|
loadCommonData
|
||||||
|
}
|
||||||
|
}
|
||||||
153
frontend/composables/useLiotHandling.ts
Normal file
153
frontend/composables/useLiotHandling.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
import type { VehicleData } from '~/services/dto/vehicle-data'
|
||||||
|
import { getDriverList } from '~/services/driver'
|
||||||
|
import { getVehicleList } from '~/services/vehicle'
|
||||||
|
import { SUPPLIER_CODE } from '~/utils/constants'
|
||||||
|
|
||||||
|
interface LiotForm {
|
||||||
|
carrierId: string
|
||||||
|
truckId: string
|
||||||
|
driverId: string
|
||||||
|
vehicleId: string
|
||||||
|
licensePlate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLiotHandling = (
|
||||||
|
form: LiotForm,
|
||||||
|
carriers: Ref<CarrierData[]>,
|
||||||
|
isHydrating: Ref<boolean>
|
||||||
|
) => {
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) return []
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||||
|
if (!form.carrierId) return []
|
||||||
|
return vehicles.value.filter(
|
||||||
|
(vehicle) =>
|
||||||
|
String(vehicle.carrier?.id) === form.carrierId &&
|
||||||
|
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-select driver/vehicle when carrier changes
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
if (isHydrating.value) return
|
||||||
|
if (!form.carrierId) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredDrivers.value.length === 1) {
|
||||||
|
form.driverId = String(filteredDrivers.value[0].id)
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate/auto-select vehicle when truck/carrier changes
|
||||||
|
watch(
|
||||||
|
() => [form.truckId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) return
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.vehicleId) return
|
||||||
|
const matches = filteredVehicles.value.some(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
form.vehicleId = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sync license plate from selected vehicle
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) return
|
||||||
|
if (isHydrating.value) return
|
||||||
|
const selected = filteredVehicles.value.find(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
form.licensePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-select vehicle from license plate
|
||||||
|
watch(
|
||||||
|
() => [form.licensePlate, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value || form.vehicleId) return
|
||||||
|
const match = filteredVehicles.value.find(
|
||||||
|
(vehicle) => vehicle.plate === form.licensePlate
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
form.vehicleId = String(match.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
drivers,
|
||||||
|
vehicles,
|
||||||
|
isLoadingDrivers,
|
||||||
|
isLoadingVehicles,
|
||||||
|
allowAnyLicensePlate,
|
||||||
|
isLiotCarrier,
|
||||||
|
filteredDrivers,
|
||||||
|
filteredVehicles,
|
||||||
|
loadDrivers,
|
||||||
|
loadVehicles
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,64 @@
|
|||||||
import type {Ref} from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import {computed, ref} from 'vue'
|
import type { 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 { createWeight, updateWeight } from '~/services/weight'
|
||||||
import {getWeightShipment} from '~/services/shipment'
|
|
||||||
import {createWeight, updateWeight} from '~/services/weight'
|
|
||||||
import type {UseWeighingShipmentOptions, UseWeighingOptions} from '~/services/weight'
|
|
||||||
import type {WeightShipmentEntryData} from "~/services/dto/shipment-data";
|
|
||||||
|
|
||||||
export type WeighingMode = 'gross' | 'tare'
|
export type WeighingMode = 'gross' | 'tare'
|
||||||
|
|
||||||
|
export interface UseWeighingOptions {
|
||||||
|
mode: WeighingMode
|
||||||
|
entity: Ref<{ id: number; currentStep: number; isValid: boolean; weights?: WeightEntryData[] | null } | null>
|
||||||
|
entityName: 'reception' | 'shipment'
|
||||||
|
apiResource: string
|
||||||
|
titleLabel: string
|
||||||
|
getWeightFromScale: () => Promise<WeightData>
|
||||||
|
updateEntity: (id: number, payload: any) => Promise<any>
|
||||||
|
loadEntity?: (id: number) => Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
export const useWeighing = ({
|
export const useWeighing = ({
|
||||||
mode,
|
mode,
|
||||||
reception,
|
entity,
|
||||||
updateReception,
|
entityName,
|
||||||
loadReception
|
apiResource,
|
||||||
}: UseWeighingOptions) => {
|
titleLabel,
|
||||||
|
getWeightFromScale,
|
||||||
|
updateEntity,
|
||||||
|
loadEntity
|
||||||
|
}: UseWeighingOptions) => {
|
||||||
const weightData = ref<WeightData | null>(null)
|
const weightData = ref<WeightData | null>(null)
|
||||||
const isFetching = ref(false)
|
const isFetching = ref(false)
|
||||||
|
|
||||||
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
const currentWeightEntry = computed<WeightEntryData | null>(() => {
|
||||||
const weights = reception.value?.weights ?? []
|
const weights = entity.value?.weights ?? []
|
||||||
return weights.find((entry) => entry.type === mode) ?? null
|
return weights.find((entry) => entry.type === mode) ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
const title = computed(() => titleLabel)
|
||||||
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
|
||||||
weightData.value = await getWeight().finally(() => {
|
weightData.value = await getWeightFromScale().finally(() => {
|
||||||
isFetching.value = false
|
isFetching.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveWeight = async () => {
|
const saveWeight = async () => {
|
||||||
if (!reception.value) {
|
if (!entity.value) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingEntry = currentWeightEntry.value
|
const existingEntry = currentWeightEntry.value
|
||||||
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
||||||
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
||||||
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||||
|
|
||||||
if (baseWeight === null) {
|
if (baseWeight === null) return
|
||||||
return
|
|
||||||
}
|
const relationPayload: Record<string, string> = {}
|
||||||
|
relationPayload[entityName] = `/api/${apiResource}/${entity.value.id}`
|
||||||
|
|
||||||
if (existingEntry?.id) {
|
if (existingEntry?.id) {
|
||||||
await updateWeight(existingEntry.id, {
|
await updateWeight(existingEntry.id, {
|
||||||
@@ -62,7 +69,7 @@ export const useWeighing = ({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await createWeight({
|
await createWeight({
|
||||||
reception: `/api/receptions/${reception.value.id}`,
|
...relationPayload,
|
||||||
type: mode,
|
type: mode,
|
||||||
dsd: baseDsd,
|
dsd: baseDsd,
|
||||||
weight: baseWeight,
|
weight: baseWeight,
|
||||||
@@ -71,15 +78,15 @@ export const useWeighing = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = mode === 'tare'
|
const nextStep = mode === 'tare'
|
||||||
? reception.value.currentStep
|
? entity.value.currentStep
|
||||||
: reception.value.currentStep + 1
|
: entity.value.currentStep + 1
|
||||||
await updateReception(reception.value.id, {
|
await updateEntity(entity.value.id, {
|
||||||
currentStep: nextStep,
|
currentStep: nextStep,
|
||||||
isValid: reception.value.isValid
|
isValid: entity.value.isValid
|
||||||
})
|
})
|
||||||
|
|
||||||
if (loadReception) {
|
if (loadEntity) {
|
||||||
await loadReception(reception.value.id)
|
await loadEntity(entity.value.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,86 +102,29 @@ export const useWeighing = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backward-compatible aliases
|
||||||
export const useWeighingShipment = ({
|
export const useWeighingShipment = ({
|
||||||
modeShipment,
|
modeShipment,
|
||||||
shipment,
|
shipment,
|
||||||
updateShipment,
|
updateShipment,
|
||||||
loadShipment
|
loadShipment
|
||||||
}: UseWeighingShipmentOptions) => {
|
}: {
|
||||||
const weightData = ref<WeightData | null>(null)
|
modeShipment: WeighingMode
|
||||||
const isFetching = ref(false)
|
shipment: Ref<any>
|
||||||
|
updateShipment: (id: number, payload: any) => Promise<any>
|
||||||
const currentWeightEntry = computed<WeightShipmentEntryData | null>(() => {
|
loadShipment?: (id: number) => Promise<any>
|
||||||
const weights = shipment.value?.weights ?? []
|
}) => {
|
||||||
return weights.find((entry) => entry.type === modeShipment) ?? null
|
return useWeighing({
|
||||||
|
mode: modeShipment,
|
||||||
|
entity: shipment,
|
||||||
|
entityName: 'shipment',
|
||||||
|
apiResource: 'shipments',
|
||||||
|
titleLabel: modeShipment === 'gross' ? 'Pesée à vide' : 'Pesée à plein',
|
||||||
|
getWeightFromScale: async () => {
|
||||||
|
const { getWeightShipment } = await import('~/services/shipment')
|
||||||
|
return getWeightShipment()
|
||||||
|
},
|
||||||
|
updateEntity: updateShipment,
|
||||||
|
loadEntity: loadShipment
|
||||||
})
|
})
|
||||||
|
|
||||||
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 || (displayWeight.value === null && currentWeightEntry.value === null)
|
|
||||||
)
|
|
||||||
|
|
||||||
const fetchWeight = async () => {
|
|
||||||
isFetching.value = true
|
|
||||||
weightData.value = await getWeightShipment().finally(() => {
|
|
||||||
isFetching.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveWeight = async () => {
|
|
||||||
if (!shipment.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingEntry = currentWeightEntry.value
|
|
||||||
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
|
||||||
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
|
||||||
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
|
||||||
|
|
||||||
if (baseWeight === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingEntry?.id) {
|
|
||||||
await updateWeight(existingEntry.id, {
|
|
||||||
type: modeShipment,
|
|
||||||
dsd: baseDsd,
|
|
||||||
weight: baseWeight,
|
|
||||||
weighedAt: baseWeighedAt
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await createWeight({
|
|
||||||
shipment: `/api/shipments/${shipment.value.id}`,
|
|
||||||
type: modeShipment,
|
|
||||||
dsd: baseDsd,
|
|
||||||
weight: baseWeight,
|
|
||||||
weighedAt: baseWeighedAt
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = modeShipment === 'tare'
|
|
||||||
? shipment.value.currentStep
|
|
||||||
: shipment.value.currentStep + 1
|
|
||||||
await updateShipment(shipment.value.id, {
|
|
||||||
currentStep: nextStep,
|
|
||||||
isValid: shipment.value.isValid
|
|
||||||
})
|
|
||||||
|
|
||||||
if (loadShipment) {
|
|
||||||
await loadShipment(shipment.value.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
weightData,
|
|
||||||
currentWeightEntry,
|
|
||||||
displayWeight,
|
|
||||||
displayDsd,
|
|
||||||
title,
|
|
||||||
showLoadingBox,
|
|
||||||
fetchWeight,
|
|
||||||
saveWeight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
83
frontend/composables/useWorkflowSteps.ts
Normal file
83
frontend/composables/useWorkflowSteps.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { WorkflowConfig } from '~/types/workflow'
|
||||||
|
|
||||||
|
interface WorkflowStore {
|
||||||
|
current: any
|
||||||
|
isLoading: boolean
|
||||||
|
clearCurrent: () => void
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWorkflowSteps = (config: WorkflowConfig, store: WorkflowStore) => {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const stepLabels = config.steps.map(s => s.label)
|
||||||
|
|
||||||
|
const currentStep = computed(() => store.current?.currentStep ?? 0)
|
||||||
|
const entity = computed(() => store.current)
|
||||||
|
|
||||||
|
const loadMethod = `load${config.entityName.charAt(0).toUpperCase() + config.entityName.slice(1)}`
|
||||||
|
const updateMethod = `update${config.entityName.charAt(0).toUpperCase() + config.entityName.slice(1)}`
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
async (param) => {
|
||||||
|
const id = resolveId(param)
|
||||||
|
if (id === null) {
|
||||||
|
store.clearCurrent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await store[loadMethod](id)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveAndHold = async () => {
|
||||||
|
if (!store.current) {
|
||||||
|
await router.push('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const datePayload: Record<string, any> = {}
|
||||||
|
datePayload[config.dateField] = store.current[config.dateField]
|
||||||
|
await store[updateMethod](store.current.id, {
|
||||||
|
currentStep: store.current.currentStep,
|
||||||
|
licensePlate: store.current.licensePlate,
|
||||||
|
...datePayload
|
||||||
|
})
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepSelect = async (step: number) => {
|
||||||
|
if (!store.current) return
|
||||||
|
if (step === store.current.currentStep) return
|
||||||
|
await store[updateMethod](store.current.id, { currentStep: step })
|
||||||
|
await store[loadMethod](store.current.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const advanceStep = async () => {
|
||||||
|
if (!store.current) return
|
||||||
|
const nextStep = store.current.currentStep + 1
|
||||||
|
await store[updateMethod](store.current.id, { currentStep: nextStep })
|
||||||
|
await store[loadMethod](store.current.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stepLabels,
|
||||||
|
currentStep,
|
||||||
|
entity,
|
||||||
|
init,
|
||||||
|
saveAndHold,
|
||||||
|
handleStepSelect,
|
||||||
|
advanceStep
|
||||||
|
}
|
||||||
|
}
|
||||||
25
frontend/config/reception.config.ts
Normal file
25
frontend/config/reception.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { WorkflowConfig, WorkflowEntity } from '~/types/workflow'
|
||||||
|
|
||||||
|
export const receptionConfig: WorkflowConfig = {
|
||||||
|
entityName: 'reception',
|
||||||
|
apiResource: 'receptions',
|
||||||
|
steps: [
|
||||||
|
{ label: 'Réception' },
|
||||||
|
{ label: 'Pesée à plein', weighingMode: 'gross' },
|
||||||
|
{ label: 'Sélection réception' },
|
||||||
|
{ label: 'Pesée à vide', weighingMode: 'tare', isFinal: true }
|
||||||
|
],
|
||||||
|
weighingLabels: {
|
||||||
|
gross: 'Pesée à plein',
|
||||||
|
tare: 'Pesée à vide'
|
||||||
|
},
|
||||||
|
buildReceiptFilename: (entity: WorkflowEntity) => {
|
||||||
|
const rec = entity as any
|
||||||
|
return `${rec.identificationNumber ?? rec.id}_${rec.supplier?.name ?? 'fournisseur'}_${rec.licensePlate ?? 'immat'}.pdf`
|
||||||
|
},
|
||||||
|
routePrefix: '/reception',
|
||||||
|
toastPrefix: 'reception',
|
||||||
|
dateField: 'receptionDate'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RECEPTION_STEP_LABELS = receptionConfig.steps.map(s => s.label)
|
||||||
25
frontend/config/shipment.config.ts
Normal file
25
frontend/config/shipment.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { WorkflowConfig, WorkflowEntity } from '~/types/workflow'
|
||||||
|
|
||||||
|
export const shipmentConfig: WorkflowConfig = {
|
||||||
|
entityName: 'shipment',
|
||||||
|
apiResource: 'shipments',
|
||||||
|
steps: [
|
||||||
|
{ label: 'Expédition' },
|
||||||
|
{ label: 'Pesée à vide', weighingMode: 'gross' },
|
||||||
|
{ label: 'Chargement' },
|
||||||
|
{ label: 'Pesée à plein', weighingMode: 'tare', isFinal: true }
|
||||||
|
],
|
||||||
|
weighingLabels: {
|
||||||
|
gross: 'Pesée à vide',
|
||||||
|
tare: 'Pesée à plein'
|
||||||
|
},
|
||||||
|
buildReceiptFilename: (entity: WorkflowEntity) => {
|
||||||
|
const ship = entity as any
|
||||||
|
return `${ship.identificationNumber ?? ship.id}_${ship.customer?.label ?? 'client'}_${ship.licensePlate ?? 'immat'}.pdf`
|
||||||
|
},
|
||||||
|
routePrefix: '/shipment',
|
||||||
|
toastPrefix: 'shipment',
|
||||||
|
dateField: 'shipmentDate'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SHIPMENT_STEP_LABELS = shipmentConfig.steps.map(s => s.label)
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
export enum StepLabel {
|
|
||||||
Reception = 'Réception',
|
|
||||||
GrossWeighing = 'Pesée à plein',
|
|
||||||
Selection = 'Sélection réception',
|
|
||||||
TareWeighing = 'Pesée à vide',
|
|
||||||
Shipment = 'Expédition',
|
|
||||||
ShipmentLoading = 'Chargement',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RECEPTION_STEP_LABELS = [
|
|
||||||
StepLabel.Reception,
|
|
||||||
StepLabel.GrossWeighing,
|
|
||||||
StepLabel.Selection,
|
|
||||||
StepLabel.TareWeighing
|
|
||||||
]
|
|
||||||
|
|
||||||
export const SHIPMENT_STEP_LABELS = [
|
|
||||||
StepLabel.Shipment,
|
|
||||||
StepLabel.TareWeighing,
|
|
||||||
StepLabel.ShipmentLoading,
|
|
||||||
StepLabel.GrossWeighing,
|
|
||||||
]
|
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"fetch": "Impossible de récupérer la réception.",
|
"fetch": "Impossible de récupérer la réception.",
|
||||||
"create": "Impossible de créer la réception.",
|
"create": "Impossible de créer la réception.",
|
||||||
"update": "Impossible de mettre à jour la réception.",
|
"update": "Impossible de mettre à jour la réception.",
|
||||||
|
"delete": "Impossible de supprimer la réception.",
|
||||||
"weight": "Impossible de récupérer la pesée."
|
"weight": "Impossible de récupérer la pesée."
|
||||||
},
|
},
|
||||||
"weight": {
|
"weight": {
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"fetch": "Impossible de récupérer l'éxpeditions.",
|
"fetch": "Impossible de récupérer l'éxpeditions.",
|
||||||
"create": "Impossible de créer l'éxpeditions.",
|
"create": "Impossible de créer l'éxpeditions.",
|
||||||
"update": "Impossible de mettre à jour l'éxpeditions.",
|
"update": "Impossible de mettre à jour l'éxpeditions.",
|
||||||
|
"delete": "Impossible de supprimer l'expédition.",
|
||||||
"weigh": "Impossible de récupérer la pesée."
|
"weigh": "Impossible de récupérer la pesée."
|
||||||
},
|
},
|
||||||
"shipmentBovine": {
|
"shipmentBovine": {
|
||||||
@@ -109,10 +111,14 @@
|
|||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"reception": {
|
"reception": {
|
||||||
"update": "Réception mise à jour avec succès."
|
"create": "Réception créée avec succès",
|
||||||
|
"update": "Réception mise à jour avec succès.",
|
||||||
|
"delete": "Réception supprimée avec succès."
|
||||||
},
|
},
|
||||||
"shipment": {
|
"shipment": {
|
||||||
"update": "Éxpedition mise à jour avec succès."
|
"create": "Éxpedition créée avec succès",
|
||||||
|
"update": "Éxpedition mise à jour avec succès.",
|
||||||
|
"delete": "Expédition supprimée avec succès."
|
||||||
},
|
},
|
||||||
"supplier": {
|
"supplier": {
|
||||||
"create": "Fournisseur créé avec succès.",
|
"create": "Fournisseur créé avec succès.",
|
||||||
|
|||||||
@@ -38,40 +38,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</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
|
<NuxtLink
|
||||||
v-if="auth.isAdmin"
|
v-if="auth.isAdmin"
|
||||||
to="/admin/user/list"
|
to="/admin/user/list"
|
||||||
@@ -89,6 +55,23 @@
|
|||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</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
|
<NuxtLink
|
||||||
v-if="auth.isAdmin"
|
v-if="auth.isAdmin"
|
||||||
to="/admin/customer/customer-list"
|
to="/admin/customer/customer-list"
|
||||||
@@ -106,6 +89,23 @@
|
|||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</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
|
<NuxtLink
|
||||||
v-if="auth.isAdmin"
|
v-if="auth.isAdmin"
|
||||||
to="/admin/bovin/bovin-list"
|
to="/admin/bovin/bovin-list"
|
||||||
@@ -134,7 +134,8 @@
|
|||||||
class="inline-flex items-center py-2 -my-2 text-xl leading-none transition hover:opacity-80"
|
class="inline-flex items-center py-2 -my-2 text-xl leading-none transition hover:opacity-80"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
>
|
>
|
||||||
<span class="capitalize font-bold">{{ userDisplayName }}</span>
|
<Icon name="mdi:account-circle-outline" class="self-center" size="36"/>
|
||||||
|
<span class="capitalize font-bold ml-4">{{ userDisplayName }}</span>
|
||||||
<span
|
<span
|
||||||
class="ml-[6px] inline-flex items-center font-bold transition-transform group-hover:rotate-180 group-focus-within:rotate-180">
|
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"/>
|
<Icon name="mdi:chevron-down" size="20"/>
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export default defineNuxtConfig({
|
|||||||
css: ['~/assets/css/main.css', '~/assets/css/toast.css'],
|
css: ['~/assets/css/main.css', '~/assets/css/toast.css'],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE
|
apiBase: process.env.NUXT_PUBLIC_API_BASE,
|
||||||
|
geoApiBase: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toast: {
|
toast: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
<div class="flex items-center justify-between relative">
|
<div class="flex items-center justify-between relative">
|
||||||
<div class="flex flex-row absolute -left-[60px]">
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -9,20 +9,21 @@
|
|||||||
class="cursor-pointer text-primary-500"
|
class="cursor-pointer text-primary-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-4xl text-primary-500 font-bold uppercase">
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
{{ route.params.id ? 'Modifications du type bovin' : 'Ajout d\'un type bovin' }}
|
{{ route.params.id ? 'Modifications du type bovin' : 'Ajout d\'un type bovin' }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 items-start pt-7 mb-11 gap-x-[200px]">
|
<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="Nom du bovin" id="bovin-label" v-model="form.label" required />
|
||||||
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" />
|
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
<UiButton
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isLoading || isHydrating"
|
: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"
|
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
>
|
>
|
||||||
Valider
|
Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
@@ -37,6 +38,7 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isHydrating = ref(false)
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
const idBovin = computed(() => resolveId(route.params.id))
|
const idBovin = computed(() => resolveId(route.params.id))
|
||||||
const isEdit = computed(() => idBovin.value !== null)
|
const isEdit = computed(() => idBovin.value !== null)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
<div class="flex items-center justify-between relative">
|
<div class="flex items-center justify-between relative">
|
||||||
<div class="flex flex-row absolute -left-[60px]">
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
<Icon
|
<Icon
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
class="cursor-pointer text-primary-500"
|
class="cursor-pointer text-primary-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-4xl text-primary-500 font-bold uppercase">
|
<h1 class="text-3xl text-primary-500 font-bold uppercase">
|
||||||
{{ route.params.id ? 'Modification du transporteur' : 'Ajout d\'un transporteur' }}
|
{{ route.params.id ? 'Modification du transporteur' : 'Ajout d\'un transporteur' }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,18 +20,21 @@
|
|||||||
label="Nom du transporteur"
|
label="Nom du transporteur"
|
||||||
id="carrier-name"
|
id="carrier-name"
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiTextInput
|
<UiTextInput
|
||||||
label="Code transporteur"
|
label="Code transporteur"
|
||||||
id="code-id"
|
id="code-id"
|
||||||
v-model="form.code"
|
v-model="form.code"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
<UiButton
|
<UiButton
|
||||||
type="submit"
|
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"
|
class="inline-flex items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
>
|
>
|
||||||
Valider
|
Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
@@ -49,6 +52,7 @@ const route = useRoute()
|
|||||||
const idCarrier = computed(() => resolveId(route.params.id))
|
const idCarrier = computed(() => resolveId(route.params.id))
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isHydrating = ref(false)
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
const resolveId = (param: unknown) => {
|
const resolveId = (param: unknown) => {
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<div class="flex flex-row absolute -left-[60px] ">
|
<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"/>
|
<Icon @click="router.push('/admin/customer/customer-list')" name="gg:arrow-left-o" size="40" class="cursor-pointer text-primary-500"/>
|
||||||
@@ -10,21 +10,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
<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-name" v-model="form.name" label="Nom du client" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!customerId" class="flex flex-cols-3 justify-between mb-11">
|
||||||
|
<UiTextInput id="address-street" v-model="addressForm.street" label="Rue" wrapper-class="w-[280px]" required />
|
||||||
|
<UiTextInput id="address-street2" v-model="addressForm.street2" label="Complément" wrapper-class="w-[280px]" />
|
||||||
|
<UiTextInput id="address-country" v-model="addressForm.countryCode" label="Pays (code)" wrapper-class="w-[280px]" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!customerId" class="flex flex-cols-3 justify-between mb-11">
|
||||||
|
<UiTextInput id="address-postalCode" v-model="addressForm.postalCode" label="Code postal" wrapper-class="w-[280px]" required />
|
||||||
|
<UiSelect id="address-city" v-model="addressForm.city" label="Ville"
|
||||||
|
:options="communeOptions" :loading="isLoadingCities"
|
||||||
|
wrapper-class="w-[280px]" required />
|
||||||
|
<div class="w-[280px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<UiButton
|
<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"
|
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"
|
type="submit"
|
||||||
:disabled="isLoading || !auth.isAdmin"
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
@click="submitted = true"
|
||||||
>
|
>
|
||||||
<Icon :name="customerId ? '' : 'mdi:plus'" size="28" />
|
<Icon :name="customerId ? '' : 'mdi:plus'" size="28" />
|
||||||
{{ customerId ? "Valider" : "Ajouter" }}
|
{{ customerId ? "Valider" : "Ajouter" }}
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="customerId">
|
||||||
<div class="flex items-center justify-between mb-7">
|
<div class="flex items-center justify-between mb-7">
|
||||||
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du client</h2>
|
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du client</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +47,6 @@
|
|||||||
<table class="w-full border-collapse text-primary-700">
|
<table class="w-full border-collapse text-primary-700">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left border bg-slate-100 border-gray-200">
|
<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">Rue</th>
|
||||||
<th class="py-3 px-4 text-sm uppercase">Complément</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">Code postal</th>
|
||||||
@@ -43,7 +57,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<template v-if="form.addresses.length === 0">
|
<template v-if="form.addresses.length === 0">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="py-4 text-slate-400">
|
<td colspan="5" class="py-4 text-slate-400">
|
||||||
Aucune adresse.
|
Aucune adresse.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -56,7 +70,6 @@
|
|||||||
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
||||||
@click="goToEditAddress(address.id ?? null)"
|
@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.street || "—" }}</td>
|
||||||
<td class="py-3 px-4">{{ address.street2 || "—" }}</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.postalCode || "—" }}</td>
|
||||||
@@ -78,6 +91,7 @@
|
|||||||
Ajouter
|
Ajouter
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,6 +99,8 @@
|
|||||||
import {computed, reactive, ref, watch} from "vue"
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
|
import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
|
||||||
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
|
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
|
||||||
|
import {createAddress, type AddressPayload} from "~/services/address"
|
||||||
|
import {getCommunesByPostalCode, type CommuneData} from "~/services/geo"
|
||||||
import {useAuthStore} from "~/stores/auth"
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -99,6 +115,7 @@ const resolveId = (param: unknown) => {
|
|||||||
}
|
}
|
||||||
const customerId = computed(() => resolveId(route.params.id))
|
const customerId = computed(() => resolveId(route.params.id))
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
const form = reactive<CustomerFormData>({
|
const form = reactive<CustomerFormData>({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
@@ -106,6 +123,30 @@ const form = reactive<CustomerFormData>({
|
|||||||
addresses: [],
|
addresses: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Address form (creation mode only)
|
||||||
|
const addressForm = reactive<AddressPayload>({
|
||||||
|
street: "", street2: null, postalCode: "", city: "", countryCode: "FR",
|
||||||
|
})
|
||||||
|
const communes = ref<CommuneData[]>([])
|
||||||
|
const isLoadingCities = ref(false)
|
||||||
|
const communeOptions = computed(() => communes.value.map(c => ({ value: c.nom, label: c.nom })))
|
||||||
|
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
watch(() => addressForm.postalCode, (cp) => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
if (!cp || cp.length < 5) { communes.value = []; addressForm.city = ''; return }
|
||||||
|
if (cp.length === 5) {
|
||||||
|
debounceTimer = setTimeout(async () => {
|
||||||
|
isLoadingCities.value = true
|
||||||
|
try {
|
||||||
|
communes.value = await getCommunesByPostalCode(cp)
|
||||||
|
if (communes.value.length === 1) addressForm.city = communes.value[0].nom
|
||||||
|
else addressForm.city = ''
|
||||||
|
} finally { isLoadingCities.value = false }
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const goToAddAddress = () => {
|
const goToAddAddress = () => {
|
||||||
if (customerId.value === null || !auth.isAdmin) return
|
if (customerId.value === null || !auth.isAdmin) return
|
||||||
router.push({
|
router.push({
|
||||||
@@ -143,7 +184,6 @@ const hydrateFromCustomer = (customer: CustomerData | null) => {
|
|||||||
|
|
||||||
form.addresses = customer.addresses.map((address) => ({
|
form.addresses = customer.addresses.map((address) => ({
|
||||||
id: address.id ?? null,
|
id: address.id ?? null,
|
||||||
label: address.label ?? "",
|
|
||||||
street: address.street ?? "",
|
street: address.street ?? "",
|
||||||
street2: address.street2 ?? null,
|
street2: address.street2 ?? null,
|
||||||
postalCode: address.postalCode ?? "",
|
postalCode: address.postalCode ?? "",
|
||||||
@@ -188,7 +228,14 @@ async function validate() {
|
|||||||
await updateCustomer(customerId.value, customerPayload)
|
await updateCustomer(customerId.value, customerPayload)
|
||||||
targetId = customerId.value
|
targetId = customerId.value
|
||||||
} else {
|
} else {
|
||||||
const created = await createCustomer(customerPayload)
|
const addressData = await createAddress({ ...addressForm })
|
||||||
|
const addressIRI = `/api/addresses/${addressData.id}`
|
||||||
|
const creationPayload = {
|
||||||
|
...customerPayload,
|
||||||
|
addresses: [addressIRI],
|
||||||
|
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
||||||
|
}
|
||||||
|
const created = await createCustomer(creationPayload)
|
||||||
targetId = created.id
|
targetId = created.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,72 +6,28 @@
|
|||||||
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
<div class="max-h-96 overflow-y-auto">
|
<div class="max-h-96 overflow-y-auto">
|
||||||
<div
|
<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"
|
class="sticky text-primary-700 top-0 z-10 grid grid-cols-4 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
>
|
>
|
||||||
<div>Nom</div>
|
<div>Nom</div>
|
||||||
<div>Téléphone</div>
|
<div>Téléphone</div>
|
||||||
<div>Email</div>
|
<div>Mail</div>
|
||||||
<div>Rue</div>
|
<div>Créé par</div>
|
||||||
<div>Complément</div>
|
|
||||||
<div>Code Postal</div>
|
|
||||||
<div>Ville</div>
|
|
||||||
<div>Pays</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400">
|
<div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
Aucun client.
|
Aucun client.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="customer in customerList" :key="customer.id">
|
<div
|
||||||
<div
|
v-for="customer in customerList"
|
||||||
v-if="!customer.addresses || customer.addresses.length === 0"
|
:key="customer.id"
|
||||||
class="grid text-primary-700 grid-cols-8 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
class="grid grid-cols-4 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
@click="goToCustomer(customer.id)"
|
@click="goToCustomer(customer.id)"
|
||||||
>
|
>
|
||||||
<div class="truncate">{{ customer.name || "—" }}</div>
|
<div class="truncate">{{ customer.name || "—" }}</div>
|
||||||
<div class="truncate">{{ customer.phone || "—" }}</div>
|
<div class="truncate">{{ customer.phone || "—" }}</div>
|
||||||
<div class="truncate">{{ customer.email || "—" }}</div>
|
<div class="truncate">{{ customer.email || "—" }}</div>
|
||||||
<div class="col-span-1">Pas d'adresse</div>
|
<div class="truncate">{{ customer.createdBy?.username || "—" }}</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
|
|
||||||
<div class="flex items-center relative">
|
<div class="flex items-center relative">
|
||||||
<div class="flex flex-row absolute -left-[60px] ">
|
<div class="flex flex-row absolute -left-[60px] ">
|
||||||
@@ -11,21 +11,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
<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-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
|
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||||
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
<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>
|
||||||
|
<div v-if="!supplierId" class="flex flex-cols-3 justify-between mb-11">
|
||||||
|
<UiTextInput id="address-street" v-model="addressForm.street" label="Rue" wrapper-class="w-[280px]" required />
|
||||||
|
<UiTextInput id="address-street2" v-model="addressForm.street2" label="Complément" wrapper-class="w-[280px]" />
|
||||||
|
<UiTextInput id="address-country" v-model="addressForm.countryCode" label="Pays (code)" wrapper-class="w-[280px]" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!supplierId" class="flex flex-cols-3 justify-between mb-11">
|
||||||
|
<UiTextInput id="address-postalCode" v-model="addressForm.postalCode" label="Code postal" wrapper-class="w-[280px]" required />
|
||||||
|
<UiSelect id="address-city" v-model="addressForm.city" label="Ville"
|
||||||
|
:options="communeOptions" :loading="isLoadingCities"
|
||||||
|
wrapper-class="w-[280px]" required />
|
||||||
|
<div class="w-[280px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<UiButton
|
<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"
|
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"
|
type="submit"
|
||||||
:disabled="isLoading || !auth.isAdmin"
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
@click="submitted = true"
|
||||||
>
|
>
|
||||||
<Icon :name="supplierId ? '' : 'mdi:plus'" size="28" />
|
<Icon :name="supplierId ? '' : 'mdi:plus'" size="28" />
|
||||||
{{ supplierId ? "Valider" : "Ajouter" }}
|
{{ supplierId ? "Valider" : "Ajouter" }}
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="supplierId">
|
||||||
<div class="flex items-center justify-between mb-7">
|
<div class="flex items-center justify-between mb-7">
|
||||||
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du fournisseur</h2>
|
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du fournisseur</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +48,6 @@
|
|||||||
<table class="w-full border-collapse">
|
<table class="w-full border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left border bg-slate-100 border-gray-200">
|
<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">Rue</th>
|
||||||
<th class="py-3 px-4 text-sm uppercase">Complément</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">Code postal</th>
|
||||||
@@ -44,7 +58,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<template v-if="form.addresses.length === 0">
|
<template v-if="form.addresses.length === 0">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="py-4 text-slate-400">
|
<td colspan="5" class="py-4 text-slate-400">
|
||||||
Aucune adresse.
|
Aucune adresse.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -57,7 +71,6 @@
|
|||||||
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
||||||
@click="goToEditAddress(address.id ?? null)"
|
@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.street || "—" }}</td>
|
||||||
<td class="py-3 px-4">{{ address.street2 || "—" }}</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.postalCode || "—" }}</td>
|
||||||
@@ -79,6 +92,7 @@
|
|||||||
Ajouter
|
Ajouter
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -86,6 +100,8 @@
|
|||||||
import {computed, reactive, ref, watch} from "vue"
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
import {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
|
import {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
|
||||||
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
|
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
|
||||||
|
import {createAddress, type AddressPayload} from "~/services/address"
|
||||||
|
import {getCommunesByPostalCode, type CommuneData} from "~/services/geo"
|
||||||
import {useAuthStore} from "~/stores/auth"
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -100,6 +116,7 @@ const resolveId = (param: unknown) => {
|
|||||||
}
|
}
|
||||||
const supplierId = computed(() => resolveId(route.params.id))
|
const supplierId = computed(() => resolveId(route.params.id))
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
const form = reactive<SupplierFormData>({
|
const form = reactive<SupplierFormData>({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
@@ -107,6 +124,30 @@ const form = reactive<SupplierFormData>({
|
|||||||
addresses: [],
|
addresses: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Address form (creation mode only)
|
||||||
|
const addressForm = reactive<AddressPayload>({
|
||||||
|
street: "", street2: null, postalCode: "", city: "", countryCode: "FR",
|
||||||
|
})
|
||||||
|
const communes = ref<CommuneData[]>([])
|
||||||
|
const isLoadingCities = ref(false)
|
||||||
|
const communeOptions = computed(() => communes.value.map(c => ({ value: c.nom, label: c.nom })))
|
||||||
|
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
watch(() => addressForm.postalCode, (cp) => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
if (!cp || cp.length < 5) { communes.value = []; addressForm.city = ''; return }
|
||||||
|
if (cp.length === 5) {
|
||||||
|
debounceTimer = setTimeout(async () => {
|
||||||
|
isLoadingCities.value = true
|
||||||
|
try {
|
||||||
|
communes.value = await getCommunesByPostalCode(cp)
|
||||||
|
if (communes.value.length === 1) addressForm.city = communes.value[0].nom
|
||||||
|
else addressForm.city = ''
|
||||||
|
} finally { isLoadingCities.value = false }
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const goToAddAddress = () => {
|
const goToAddAddress = () => {
|
||||||
if (supplierId.value === null || !auth.isAdmin) return
|
if (supplierId.value === null || !auth.isAdmin) return
|
||||||
router.push({
|
router.push({
|
||||||
@@ -146,7 +187,6 @@ const hydrateFromSupplier = (supplier: SupplierData | null) => {
|
|||||||
|
|
||||||
form.addresses = supplier.addresses.map((address) => ({
|
form.addresses = supplier.addresses.map((address) => ({
|
||||||
id: address.id ?? null,
|
id: address.id ?? null,
|
||||||
label: address.label ?? "",
|
|
||||||
street: address.street ?? "",
|
street: address.street ?? "",
|
||||||
street2: address.street2 ?? null,
|
street2: address.street2 ?? null,
|
||||||
postalCode: address.postalCode ?? "",
|
postalCode: address.postalCode ?? "",
|
||||||
@@ -191,7 +231,14 @@ async function validate() {
|
|||||||
await updateSupplier(supplierId.value, supplierPayload)
|
await updateSupplier(supplierId.value, supplierPayload)
|
||||||
targetId = supplierId.value
|
targetId = supplierId.value
|
||||||
} else {
|
} else {
|
||||||
const created = await createSupplier(supplierPayload)
|
const addressData = await createAddress({ ...addressForm })
|
||||||
|
const addressIRI = `/api/addresses/${addressData.id}`
|
||||||
|
const creationPayload = {
|
||||||
|
...supplierPayload,
|
||||||
|
addresses: [addressIRI],
|
||||||
|
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
||||||
|
}
|
||||||
|
const created = await createSupplier(creationPayload)
|
||||||
targetId = created.id
|
targetId = created.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,68 +6,28 @@
|
|||||||
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
<div class="max-h-96 overflow-y-auto">
|
<div class="max-h-96 overflow-y-auto">
|
||||||
<div
|
<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"
|
class="sticky text-primary-700 top-0 z-10 grid grid-cols-4 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
>
|
>
|
||||||
<div>Nom</div>
|
<div>Nom</div>
|
||||||
|
<div>Téléphone</div>
|
||||||
<div>Mail</div>
|
<div>Mail</div>
|
||||||
<div>Rue</div>
|
<div>Créé par</div>
|
||||||
<div>Complément</div>
|
|
||||||
<div>Code Postal</div>
|
|
||||||
<div>Ville</div>
|
|
||||||
<div>Pays</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="supplierList.length === 0" class="px-4 py-6 text-slate-400">
|
<div v-if="supplierList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
Aucun fournisseur.
|
Aucun fournisseur.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="supplier in supplierList" :key="supplier.id">
|
<div
|
||||||
<div
|
v-for="supplier in supplierList"
|
||||||
v-if="!supplier.addresses || supplier.addresses.length === 0"
|
:key="supplier.id"
|
||||||
class="grid text-primary-700 grid-cols-7 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
class="grid grid-cols-4 text-primary-700 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
@click="goToSupplier(supplier.id)"
|
@click="goToSupplier(supplier.id)"
|
||||||
>
|
>
|
||||||
<div class="truncate">{{ supplier.name }}</div>
|
<div class="truncate">{{ supplier.name || "—" }}</div>
|
||||||
<div class="truncate">{{ supplier.email }}</div>
|
<div class="truncate">{{ supplier.phone || "—" }}</div>
|
||||||
<div class="col-span-1">Pas d'adresse</div>
|
<div class="truncate">{{ supplier.email || "—" }}</div>
|
||||||
<div class="uppercase truncate">{{"—"}}</div>
|
<div class="truncate">{{ supplier.createdBy?.username || "—" }}</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,54 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
<div
|
<div class="flex items-center relative">
|
||||||
class="flex items-center justify-between gap-10">
|
<div class="flex flex-row absolute -left-[60px]">
|
||||||
<h1 class="text-3xl font-bold uppercase">
|
<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" }}
|
{{ userId ? "Modification de l'utilisateur" : "Ajout d'un utilisateur" }}
|
||||||
</h1>
|
</h1>
|
||||||
<UiButton
|
|
||||||
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"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<Icon :name="userId ? '' : 'mdi:plus'" size="28" />
|
|
||||||
{{ userId ? 'Valider' : 'Ajouter' }}
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-y-16 gap-x-40 py-12">
|
<div class="flex flex-cols-3 justify-between mb-11 pt-7">
|
||||||
<UiTextInput
|
<UiTextInput
|
||||||
id="user-name"
|
id="user-name"
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
label="Nom de l'utilisateur"
|
label="Nom de l'utilisateur"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="user-role"
|
id="user-role"
|
||||||
v-model="form.role"
|
v-model="form.role"
|
||||||
label="Rôle de l'utilisateur"
|
label="Role de l'utilisateur"
|
||||||
:options="ROLE"
|
:options="ROLE"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiTextInput
|
<UiTextInput
|
||||||
id="user-password"
|
id="user-password"
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
label="Mot de passe"
|
label="Mot de passe"
|
||||||
type="password"
|
type="password"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
wrapper-class="w-[280px]"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
</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"
|
||||||
|
@click="submitted = true"
|
||||||
|
>
|
||||||
|
<Icon :name="userId ? '' : 'mdi:plus'" size="28" />
|
||||||
|
{{ userId ? 'Valider' : 'Ajouter' }}
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, reactive, ref, watch} from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import {ROLE} from '~/utils/constants'
|
import { ROLE } from '~/utils/constants'
|
||||||
import {createUser, updateUser, getUser} from '~/services/auth'
|
import { createUser, updateUser, getUser } from '~/services/auth'
|
||||||
import type {UserData, UserFormData, UserPayload} from '~/services/dto/user-data'
|
import type { UserData, UserFormData, UserPayload } from '~/services/dto/user-data'
|
||||||
|
import { useAuthStore } from '~/stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
const userId = computed(() => resolveUserId(route.params.id))
|
const userId = computed(() => resolveUserId(route.params.id))
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isHydrating = ref(false)
|
const isHydrating = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
const resolveUserId = (param: unknown) => {
|
const resolveUserId = (param: unknown) => {
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
@@ -59,7 +83,6 @@ const resolveUserId = (param: unknown) => {
|
|||||||
return Number.isFinite(id) ? id : null
|
return Number.isFinite(id) ? id : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const form = reactive<UserFormData>({
|
const form = reactive<UserFormData>({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -73,8 +96,8 @@ const hydrateFromUser = (user: UserData | null) => {
|
|||||||
isHydrating.value = true
|
isHydrating.value = true
|
||||||
form.username = user.username ?? ''
|
form.username = user.username ?? ''
|
||||||
const roles = user.roles ?? []
|
const roles = user.roles ?? []
|
||||||
const hasAdmin = roles.includes("ROLE_ADMIN")
|
const hasAdmin = roles.includes('ROLE_ADMIN')
|
||||||
form.role = hasAdmin ? "ROLE_ADMIN" : "ROLE_USER"
|
form.role = hasAdmin ? 'ROLE_ADMIN' : 'ROLE_USER'
|
||||||
form.password = ''
|
form.password = ''
|
||||||
isHydrating.value = false
|
isHydrating.value = false
|
||||||
}
|
}
|
||||||
@@ -93,10 +116,11 @@ watch(
|
|||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
async function validate() {
|
async function validate() {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
|
||||||
const normalizedUsername = form.username.trim()
|
const normalizedUsername = form.username.trim()
|
||||||
const normalizedRole = form.role.trim()
|
const normalizedRole = form.role.trim()
|
||||||
@@ -112,13 +136,12 @@ async function validate() {
|
|||||||
|
|
||||||
if (userId.value) {
|
if (userId.value) {
|
||||||
await updateUser(userId.value, basePayload)
|
await updateUser(userId.value, basePayload)
|
||||||
await router.push(`/admin/user/list/`)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await createUser(basePayload)
|
const created = await createUser(basePayload)
|
||||||
if (created) {
|
if (created) {
|
||||||
await router.push(`/admin/user/list/`)
|
await router.push('/admin/user/list')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,58 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-3xl font-bold uppercase text-primary-500">Liste des utilisateurs</h1>
|
<h1 class="text-4xl font-bold uppercase text-primary-500">Liste des utilisateurs</h1>
|
||||||
<NuxtLink
|
|
||||||
to="/admin/user"
|
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
<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 class="grid grid-cols-3 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
<div>Utilisateur</div>
|
||||||
<div>Username</div>
|
<div>Role</div>
|
||||||
<div>Role</div>
|
</div>
|
||||||
</div>
|
<div v-if="userList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun utilisateur.
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="user in userList"
|
v-for="user in userList"
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
class="grid grid-cols-3 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t items-center"
|
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"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="goToUser(user.id)"
|
@click="goToUser(user.id)"
|
||||||
|
@keydown.enter="goToUser(user.id)"
|
||||||
>
|
>
|
||||||
<div>
|
<div>{{ user.username }}</div>
|
||||||
{{ user.username }}
|
<div>{{ getRoleLabels(user.roles) }}</div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ getRoleLabels(user.roles) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {UserData} from "~/services/dto/user-data";
|
import type { UserData } from "~/services/dto/user-data"
|
||||||
import {getAdminUsers} from "~/services/auth";
|
import { getAdminUsers } from "~/services/auth"
|
||||||
import {ROLE} from "~/utils/constants";
|
import { ROLE } from "~/utils/constants"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
const userList = ref<UserData[]>([])
|
const userList = ref<UserData[]>([])
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
const roleLabelByValue = new Map(ROLE.map((role) => [role.value, role.label]))
|
const roleLabelByValue = new Map(ROLE.map((role) => [role.value, role.label]))
|
||||||
|
|
||||||
const goToUser = (id: number) => {
|
const goToUser = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
router.push(`/admin/user/${id}`)
|
router.push(`/admin/user/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
const getRoleLabels = (roles?: string[]) => {
|
const getRoleLabels = (roles?: string[]) => {
|
||||||
if (!roles || roles.length === 0) {
|
if (!roles || roles.length === 0) {
|
||||||
return ' ---'
|
return '---'
|
||||||
}
|
}
|
||||||
|
|
||||||
return roles
|
return roles
|
||||||
@@ -61,6 +75,7 @@ const getRoleLabels = (roles?: string[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
userList.value = await getAdminUsers()
|
userList.value = await getAdminUsers()
|
||||||
})
|
})
|
||||||
</script>
|
</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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<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="stepLabels"
|
||||||
:current-step="storeReception?.currentStep ?? 0"
|
:current-step="storeReception?.currentStep ?? 0"
|
||||||
@select="handleStepSelect"
|
@select="handleStepSelect"
|
||||||
/>
|
/>
|
||||||
@@ -15,76 +15,70 @@
|
|||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
<WorkflowWeight
|
||||||
|
v-if="storeReception?.currentStep === 1"
|
||||||
|
mode="gross"
|
||||||
|
entity-name="reception"
|
||||||
|
api-resource="receptions"
|
||||||
|
:title-label="receptionConfig.weighingLabels.gross"
|
||||||
|
:is-final="false"
|
||||||
|
:entity="storeReception"
|
||||||
|
:get-weight-from-scale="getWeight"
|
||||||
|
:update-entity="receptionStore.updateReception"
|
||||||
|
:load-entity="receptionStore.loadReception"
|
||||||
|
:clear-entity="receptionStore.clearCurrent"
|
||||||
|
:build-receipt-filename="receptionConfig.buildReceiptFilename"
|
||||||
|
/>
|
||||||
<ReceptionProductReceived
|
<ReceptionProductReceived
|
||||||
v-if="storeReception?.currentStep === 2 &&
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
|
||||||
<ReceptionBovineReceived
|
<ReceptionBovineReceived
|
||||||
v-if="storeReception?.currentStep === 2 &&
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
<WorkflowWeight
|
||||||
|
v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3"
|
||||||
|
mode="tare"
|
||||||
|
entity-name="reception"
|
||||||
|
api-resource="receptions"
|
||||||
|
:title-label="receptionConfig.weighingLabels.tare"
|
||||||
|
:is-final="true"
|
||||||
|
:entity="storeReception"
|
||||||
|
:get-weight-from-scale="getWeight"
|
||||||
|
:update-entity="receptionStore.updateReception"
|
||||||
|
:load-entity="receptionStore.loadReception"
|
||||||
|
:clear-entity="receptionStore.clearCurrent"
|
||||||
|
:build-receipt-filename="receptionConfig.buildReceiptFilename"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useReceptionStore} from '~/stores/reception'
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
import {storeToRefs} from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import {RECEPTION_STEP_LABELS} from '~/constants/steps'
|
import { useWorkflowSteps } from '~/composables/useWorkflowSteps'
|
||||||
import {RECEPTION_TYPE_CODES} from "~/utils/constants";
|
import { receptionConfig } from '~/config/reception.config'
|
||||||
|
import { getWeight } from '~/services/reception'
|
||||||
const route = useRoute()
|
import { RECEPTION_TYPE_CODES } from '~/utils/constants'
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const {current: storeReception} = storeToRefs(receptionStore)
|
const { current: storeReception } = storeToRefs(receptionStore)
|
||||||
|
|
||||||
const resolveReceptionId = (param: unknown) => {
|
const { stepLabels, saveAndHold, handleStepSelect } = useWorkflowSteps(receptionConfig, receptionStore)
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
|
||||||
if (!idStr) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Init route watcher
|
||||||
|
const route = useRoute()
|
||||||
watch(
|
watch(
|
||||||
() => route.params.id,
|
() => route.params.id,
|
||||||
async (param) => {
|
async (param) => {
|
||||||
const id = resolveReceptionId(param)
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
if (id === null) {
|
if (!idStr) {
|
||||||
receptionStore.clearCurrent()
|
receptionStore.clearCurrent()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await receptionStore.loadReception(id)
|
const id = Number(idStr)
|
||||||
|
if (Number.isFinite(id)) {
|
||||||
|
await receptionStore.loadReception(id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveAndHold = async () => {
|
|
||||||
if (!receptionStore.current) {
|
|
||||||
await router.push('/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
currentStep: receptionStore.current.currentStep,
|
|
||||||
licensePlate: receptionStore.current.licensePlate,
|
|
||||||
receptionDate: receptionStore.current.receptionDate
|
|
||||||
})
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleStepSelect = async (step: number) => {
|
|
||||||
if (!receptionStore.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === receptionStore.current.currentStep) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await receptionStore.updateReception(receptionStore.current.id, {
|
|
||||||
currentStep: step
|
|
||||||
})
|
|
||||||
await receptionStore.loadReception(receptionStore.current.id)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<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>
|
||||||
<div>Date</div>
|
<div>Date et heure</div>
|
||||||
<div>Fournisseur</div>
|
<div>Fournisseur</div>
|
||||||
<div>Adresse</div>
|
<div>Adresse</div>
|
||||||
<div>Type réception</div>
|
<div>Type réception</div>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
@click="goToReception(reception.id)"
|
@click="goToReception(reception.id)"
|
||||||
>
|
>
|
||||||
<div>{{ reception.identificationNumber}}</div>
|
<div>{{ reception.identificationNumber}}</div>
|
||||||
<div>{{ reception.receptionDate}}</div>
|
<div>{{ formatDate(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>
|
||||||
@@ -41,6 +41,19 @@ import type {ShipmentData} from "~/services/dto/shipment-data";
|
|||||||
const receptionList = ref<ReceptionData[]>()
|
const receptionList = ref<ReceptionData[]>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formatDate = (date: string | null) => {
|
||||||
|
if (!date) return '—'
|
||||||
|
const d = new Date(date.replace(' ', 'T'))
|
||||||
|
if (isNaN(d.getTime())) return date
|
||||||
|
return d.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const formatWeighing = (reception: ReceptionData) => {
|
const formatWeighing = (reception: ReceptionData) => {
|
||||||
const gross = reception.weights?.find((weight) => weight.type === 'gross')?.weight
|
const gross = reception.weights?.find((weight) => weight.type === 'gross')?.weight
|
||||||
const tare = reception.weights?.find((weight) => weight.type === 'tare')?.weight
|
const tare = reception.weights?.find((weight) => weight.type === 'tare')?.weight
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-[60px]">
|
<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 items-center justify-between gap-10 relative">
|
||||||
<div class="flex flex-row absolute -left-[60px] justify-between">
|
<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"/>
|
<Icon @click="router.push('/reception/finish-reception')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Réception {{ form.identificationNumber }}</h1>
|
<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 class="bg-primary-500 p-1 rounded-md flex items-center" title="Imprimer" @click="printReceipt">
|
||||||
|
<Icon name="mdi:printer-outline" size="32" class="cursor-pointer text-white"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Nom de l'utilisateur -->
|
<!-- Nom de l'utilisateur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingUsers"
|
:loading="isLoadingUsers"
|
||||||
wrapper-class="col-start-1 row-start-2"
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Date de réception -->
|
<!-- Date de réception -->
|
||||||
<UiDateInput
|
<UiDateInput
|
||||||
@@ -29,6 +32,7 @@
|
|||||||
v-model="form.receptionDate"
|
v-model="form.receptionDate"
|
||||||
label="Date de réception"
|
label="Date de réception"
|
||||||
wrapper-class="col-start-1 row-start-3"
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- type de reception -->
|
<!-- type de reception -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -42,6 +46,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingSuppliers"
|
:loading="isLoadingSuppliers"
|
||||||
wrapper-class="col-start-1 row-start-4"
|
wrapper-class="col-start-1 row-start-4"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Fournisseur -->
|
<!-- Fournisseur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -55,6 +60,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingSuppliers"
|
:loading="isLoadingSuppliers"
|
||||||
wrapper-class="col-start-1 row-start-5"
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Adresse fournisseur -->
|
<!-- Adresse fournisseur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -67,6 +73,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:disabled="(isLoadingSuppliers || supplierAddresses.length === 0) && !auth.isAdmin"
|
:disabled="(isLoadingSuppliers || supplierAddresses.length === 0) && !auth.isAdmin"
|
||||||
wrapper-class="col-start-2 row-start-1"
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Camion -->
|
<!-- Camion -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -80,6 +87,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingTrucks"
|
:loading="isLoadingTrucks"
|
||||||
wrapper-class="col-start-2 row-start-2"
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Transporteur -->
|
<!-- Transporteur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -94,6 +102,7 @@
|
|||||||
:loading="isLoadingCarriers"
|
:loading="isLoadingCarriers"
|
||||||
select-class="h-[34px]"
|
select-class="h-[34px]"
|
||||||
wrapper-class="col-start-2 row-start-3"
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Chauffeur (LIOT) -->
|
<!-- Chauffeur (LIOT) -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -108,6 +117,7 @@
|
|||||||
:loading="isLoadingDrivers"
|
:loading="isLoadingDrivers"
|
||||||
wrapper-class="col-start-2 row-start-5"
|
wrapper-class="col-start-2 row-start-5"
|
||||||
v-if="isLiotCarrier"
|
v-if="isLiotCarrier"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<!-- Plaque d'immatriculation -->
|
<!-- Plaque d'immatriculation -->
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
@@ -115,6 +125,7 @@
|
|||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Immatriculation (LIOT) -->
|
<!-- Immatriculation (LIOT) -->
|
||||||
@@ -130,27 +141,37 @@
|
|||||||
:loading="isLoadingVehicles"
|
:loading="isLoadingVehicles"
|
||||||
:disabled="(isLoadingVehicles || filteredVehicles.length === 0) && !auth.isAdmin"
|
:disabled="(isLoadingVehicles || filteredVehicles.length === 0) && !auth.isAdmin"
|
||||||
wrapper-class="col-start-2 row-start-4"
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formIsLoading">
|
<div v-if="formIsLoading">
|
||||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||||
<h1
|
<h1
|
||||||
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer "
|
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'"
|
:class="[
|
||||||
|
activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||||
|
hasGrossWeightError ? '!text-red-500 !border-red-500' : ''
|
||||||
|
]"
|
||||||
@click="activeTab = 'weights'"
|
@click="activeTab = 'weights'"
|
||||||
>
|
>
|
||||||
pesée à plein
|
pesée à plein
|
||||||
</h1>
|
</h1>
|
||||||
<h1
|
<h1
|
||||||
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer "
|
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'"
|
:class="[
|
||||||
|
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||||
|
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
||||||
|
]"
|
||||||
@click="activeTab = 'weightsEmpty'"
|
@click="activeTab = 'weightsEmpty'"
|
||||||
>
|
>
|
||||||
pesée à vide
|
pesée à vide
|
||||||
</h1>
|
</h1>
|
||||||
<h1
|
<h1
|
||||||
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer "
|
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'"
|
:class="[
|
||||||
|
activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||||
|
hasMerchandiseTabError ? '!text-red-500 !border-red-500' : ''
|
||||||
|
]"
|
||||||
@click="activeTab = 'merchandise'"
|
@click="activeTab = 'merchandise'"
|
||||||
>
|
>
|
||||||
{{ isMerchandise ? "Marchandise" : "Bovins" }}
|
{{ isMerchandise ? "Marchandise" : "Bovins" }}
|
||||||
@@ -176,6 +197,9 @@
|
|||||||
v-model="merchandiseForm"
|
v-model="merchandiseForm"
|
||||||
:isAdmin="auth.isAdmin"
|
:isAdmin="auth.isAdmin"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="activeTab === 'merchandise' && isMerchandise" class="text-red-500 text-sm mt-2" :class="showMerchandiseError ? '' : 'invisible'">
|
||||||
|
{{ merchandiseErrorMessage || ' ' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<update-bovin
|
<update-bovin
|
||||||
v-if="activeTab === 'merchandise' && !isMerchandise"
|
v-if="activeTab === 'merchandise' && !isMerchandise"
|
||||||
@@ -183,12 +207,16 @@
|
|||||||
v-model:otherQuantity="bovineOtherQuantity"
|
v-model:otherQuantity="bovineOtherQuantity"
|
||||||
:isAdmin="auth.isAdmin"
|
:isAdmin="auth.isAdmin"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="activeTab === 'merchandise' && !isMerchandise" class="text-red-500 text-sm mt-2" :class="showMerchandiseError ? '' : 'invisible'">
|
||||||
|
{{ merchandiseErrorMessage || ' ' }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UiButton
|
<UiButton
|
||||||
v-if="auth.isAdmin"
|
v-if="auth.isAdmin"
|
||||||
type="submit"
|
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"
|
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"
|
||||||
|
@click="submitted = true"
|
||||||
>
|
>
|
||||||
Valider
|
Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
@@ -239,7 +267,9 @@ import { getVehicleList } from '~/services/vehicle'
|
|||||||
import { createWeight, updateWeight } from '~/services/weight'
|
import { createWeight, updateWeight } from '~/services/weight'
|
||||||
import { useAuthStore } from '~/stores/auth'
|
import { useAuthStore } from '~/stores/auth'
|
||||||
import { useReceptionStore } from '~/stores/reception'
|
import { useReceptionStore } from '~/stores/reception'
|
||||||
import { RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
|
import { MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
|
||||||
|
import { getMerchandiseTypeList } from '~/services/merchandise-type'
|
||||||
|
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -260,6 +290,17 @@ const merchandiseForm = ref<MerchandiseEntryData>({
|
|||||||
selectedPelletBuildingIds: {}
|
selectedPelletBuildingIds: {}
|
||||||
})
|
})
|
||||||
const allowAnyLicensePlate = ref(false)
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
const showMerchandiseError = ref(false)
|
||||||
|
const merchandiseErrorMessage = ref('')
|
||||||
|
|
||||||
|
const hasGrossWeightError = computed(() =>
|
||||||
|
submitted.value && (grossWeight.value.weight === null || grossWeight.value.weighedAt === null || grossWeight.value.dsd === null)
|
||||||
|
)
|
||||||
|
const hasTareWeightError = computed(() =>
|
||||||
|
submitted.value && (tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null)
|
||||||
|
)
|
||||||
|
const hasMerchandiseTabError = computed(() => submitted.value && showMerchandiseError.value)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const users = ref<UserData[]>([])
|
const users = ref<UserData[]>([])
|
||||||
const isLoadingUsers = ref(false)
|
const isLoadingUsers = ref(false)
|
||||||
@@ -277,6 +318,7 @@ const vehicles = ref<VehicleData[]>([])
|
|||||||
const isLoadingVehicles = ref(false)
|
const isLoadingVehicles = ref(false)
|
||||||
const formIsLoading = ref(false)
|
const formIsLoading = ref(false)
|
||||||
const isMerchandise = ref(false)
|
const isMerchandise = ref(false)
|
||||||
|
const merchandiseTypesList = ref<MerchandiseTypeData[]>([])
|
||||||
const isHydrating = ref(false)
|
const isHydrating = ref(false)
|
||||||
const vehicleSyncLock = ref(false)
|
const vehicleSyncLock = ref(false)
|
||||||
|
|
||||||
@@ -436,7 +478,7 @@ function hydrateFromReception(reception: ReceptionData | null) {
|
|||||||
isHydrating.value = true
|
isHydrating.value = true
|
||||||
form.identificationNumber = reception?.identificationNumber ?? ''
|
form.identificationNumber = reception?.identificationNumber ?? ''
|
||||||
form.licensePlate = reception?.licensePlate ?? ''
|
form.licensePlate = reception?.licensePlate ?? ''
|
||||||
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
|
form.receptionDate = reception?.receptionDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
|
||||||
form.userId = reception?.user?.id
|
form.userId = reception?.user?.id
|
||||||
? String(reception.user.id)
|
? String(reception.user.id)
|
||||||
: form.userId
|
: form.userId
|
||||||
@@ -776,6 +818,53 @@ async function validate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (idReception) {
|
if (idReception) {
|
||||||
|
const hasInvalidWeights =
|
||||||
|
grossWeight.value.weight === null || grossWeight.value.weighedAt === null || grossWeight.value.dsd === null ||
|
||||||
|
tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null
|
||||||
|
|
||||||
|
if (hasInvalidWeights) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showMerchandiseError.value = false
|
||||||
|
merchandiseErrorMessage.value = ''
|
||||||
|
|
||||||
|
if (!isMerchandise.value && getTotalBovines() === 0) {
|
||||||
|
showMerchandiseError.value = true
|
||||||
|
merchandiseErrorMessage.value = 'Veuillez saisir au moins une race bovine.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMerchandise.value) {
|
||||||
|
const selectedType = merchandiseTypesList.value.find(
|
||||||
|
(t) => String(t.id) === merchandiseForm.value.merchandiseTypeId
|
||||||
|
)
|
||||||
|
const isAutresType = selectedType?.code === MERCHANDISE_TYPE_CODES.AUTRES
|
||||||
|
const isGranuleType = selectedType?.code === MERCHANDISE_TYPE_CODES.GRANULE
|
||||||
|
|
||||||
|
if (isAutresType && !merchandiseForm.value.merchandiseDetail.trim()) {
|
||||||
|
showMerchandiseError.value = true
|
||||||
|
merchandiseErrorMessage.value = 'Veuillez préciser le type de marchandise.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAutresType && !isGranuleType && merchandiseForm.value.selectedBuildingIds.length === 0) {
|
||||||
|
showMerchandiseError.value = true
|
||||||
|
merchandiseErrorMessage.value = 'Veuillez sélectionner au moins un bâtiment.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGranuleType) {
|
||||||
|
const hasAny = Object.values(merchandiseForm.value.selectedPelletBuildingIds)
|
||||||
|
.some((ids) => ids.length > 0)
|
||||||
|
if (!hasAny) {
|
||||||
|
showMerchandiseError.value = true
|
||||||
|
merchandiseErrorMessage.value = 'Veuillez sélectionner au moins un bâtiment.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await receptionStore.updateReception(idReception, {
|
await receptionStore.updateReception(idReception, {
|
||||||
...payload
|
...payload
|
||||||
})
|
})
|
||||||
@@ -820,6 +909,7 @@ async function validate() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadTypes()
|
await loadTypes()
|
||||||
|
merchandiseTypesList.value = await getMerchandiseTypeList()
|
||||||
syncMerchandiseFlag()
|
syncMerchandiseFlag()
|
||||||
formIsLoading.value = true
|
formIsLoading.value = true
|
||||||
await loadUsers()
|
await loadUsers()
|
||||||
|
|||||||
@@ -1,48 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<WorkflowWaitingList
|
||||||
<div class="flex items-center gap-10">
|
title="listes des réceptions en attente"
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
:columns="columns"
|
||||||
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions en attente</h1>
|
:items="receptionList ?? []"
|
||||||
</div>
|
route-prefix="/reception"
|
||||||
</div>
|
show-actions
|
||||||
|
>
|
||||||
<div class="px-[86px]">
|
<template #cell-receptionDate="{ item }">
|
||||||
<div class="mt-6 border border-slate-200 mb-16">
|
{{ formatDate(item.receptionDate) }}
|
||||||
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
</template>
|
||||||
<div>Fournisseur</div>
|
<template #actions="{ item }">
|
||||||
<div>Adresse</div>
|
<Icon
|
||||||
<div>Type réception</div>
|
name="mdi:delete-outline"
|
||||||
<div>Transporteur</div>
|
size="24"
|
||||||
<div>Immatriculation</div>
|
class="cursor-pointer text-red-500 hover:text-red-700"
|
||||||
</div>
|
@click="confirmDelete(item)"
|
||||||
<div
|
/>
|
||||||
v-for="reception in receptionList"
|
</template>
|
||||||
:key="reception.id"
|
</WorkflowWaitingList>
|
||||||
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="goToReception(reception.id)"
|
|
||||||
@keydown.enter="goToReception(reception.id)"
|
|
||||||
>
|
|
||||||
<div>{{ reception.supplier?.name }}</div>
|
|
||||||
<div>{{ reception.address?.fullAddress }}</div>
|
|
||||||
<div>{{ reception.receptionType?.label }}</div>
|
|
||||||
<div>{{ reception.carrier?.name }}</div>
|
|
||||||
<div>{{ reception.licensePlate }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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, deleteReception } from '~/services/reception'
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: 'receptionDate', label: 'Date et heure' },
|
||||||
|
{ key: 'supplier.name', label: 'Fournisseur' },
|
||||||
|
{ key: 'address.fullAddress', label: 'Adresse' },
|
||||||
|
{ key: 'receptionType.label', label: 'Type réception' },
|
||||||
|
{ key: 'carrier.name', label: 'Transporteur' },
|
||||||
|
{ key: 'licensePlate', label: 'Immatriculation' }
|
||||||
|
]
|
||||||
|
|
||||||
const receptionList = ref<ReceptionData[]>()
|
const receptionList = ref<ReceptionData[]>()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const goToReception = (id: number) => {
|
const formatDate = (date: string | null) => {
|
||||||
router.push(`/reception/${id}`)
|
if (!date) return '—'
|
||||||
|
const d = new Date(date.replace(' ', 'T'))
|
||||||
|
if (isNaN(d.getTime())) return date
|
||||||
|
return d.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async (reception: ReceptionData) => {
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
`Êtes-vous sûr de vouloir supprimer la réception ${reception.identificationNumber ?? `#${reception.id}`} ? Toutes les données liées seront supprimées.`
|
||||||
|
)
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
await deleteReception(reception.id)
|
||||||
|
receptionList.value = receptionList.value?.filter(r => r.id !== reception.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
<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="SHIPMENT_STEP_LABELS"
|
:labels="stepLabels"
|
||||||
:current-step="storeShipment?.currentStep ?? 0"
|
:current-step="storeShipment?.currentStep ?? 0"
|
||||||
@select="handleStepSelect"
|
@select="handleStepSelect"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UiButton
|
<UiButton
|
||||||
@@ -17,45 +16,69 @@
|
|||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
||||||
<ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
|
<WorkflowWeight
|
||||||
|
v-if="storeShipment?.currentStep === 1"
|
||||||
|
mode="gross"
|
||||||
|
entity-name="shipment"
|
||||||
|
api-resource="shipments"
|
||||||
|
:title-label="shipmentConfig.weighingLabels.gross"
|
||||||
|
:is-final="false"
|
||||||
|
:entity="storeShipment"
|
||||||
|
:get-weight-from-scale="getWeightShipment"
|
||||||
|
:update-entity="shipmentStore.updateShipment"
|
||||||
|
:load-entity="shipmentStore.loadShipment"
|
||||||
|
:clear-entity="shipmentStore.clearCurrent"
|
||||||
|
:build-receipt-filename="shipmentConfig.buildReceiptFilename"
|
||||||
|
/>
|
||||||
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
||||||
<ShipmentWeight v-if="storeShipment?.currentStep === 3" mode="tare"/>
|
<WorkflowWeight
|
||||||
|
v-if="storeShipment?.currentStep === 3"
|
||||||
|
mode="tare"
|
||||||
|
entity-name="shipment"
|
||||||
|
api-resource="shipments"
|
||||||
|
:title-label="shipmentConfig.weighingLabels.tare"
|
||||||
|
:is-final="true"
|
||||||
|
:entity="storeShipment"
|
||||||
|
:get-weight-from-scale="getWeightShipment"
|
||||||
|
:update-entity="shipmentStore.updateShipment"
|
||||||
|
:load-entity="shipmentStore.loadShipment"
|
||||||
|
:clear-entity="shipmentStore.clearCurrent"
|
||||||
|
:build-receipt-filename="shipmentConfig.buildReceiptFilename"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import {SHIPMENT_STEP_LABELS} from "~/constants/steps";
|
<script setup lang="ts">
|
||||||
import {storeToRefs} from "pinia";
|
import { storeToRefs } from 'pinia'
|
||||||
import {useShipmentStore} from "~/stores/shipment";
|
import { useShipmentStore } from '~/stores/shipment'
|
||||||
|
import { useWorkflowSteps } from '~/composables/useWorkflowSteps'
|
||||||
|
import { shipmentConfig } from '~/config/shipment.config'
|
||||||
|
import { getWeightShipment } from '~/services/shipment'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
const shipmentStore = useShipmentStore()
|
const shipmentStore = useShipmentStore()
|
||||||
const {current: storeShipment} = storeToRefs(shipmentStore)
|
const { current: storeShipment } = storeToRefs(shipmentStore)
|
||||||
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
|
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
|
||||||
|
|
||||||
|
const { stepLabels, handleStepSelect } = useWorkflowSteps(shipmentConfig, shipmentStore)
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const resolveShipmentId = (param: unknown) => {
|
watch(
|
||||||
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,
|
() => route.params.id,
|
||||||
async (param) => {
|
async (param) => {
|
||||||
const id = resolveShipmentId(param)
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
if (id === null) {
|
if (!idStr) {
|
||||||
shipmentStore.clearCurrent()
|
shipmentStore.clearCurrent()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await shipmentStore.loadShipment(id)
|
const id = Number(idStr)
|
||||||
|
if (Number.isFinite(id)) {
|
||||||
|
await shipmentStore.loadShipment(id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveAndHold = async () => {
|
const saveAndHold = async () => {
|
||||||
@@ -64,20 +87,4 @@ const saveAndHold = async () => {
|
|||||||
}
|
}
|
||||||
await router.push('/')
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form :class="{ submitted }" @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
<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 items-center justify-between gap-10 relative col-start-1 row-start-1">
|
||||||
<div class="flex flex-row absolute -left-[60px] justify-between">
|
<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"/>
|
<Icon @click="router.push('/shipment/finish-shipment')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Expédition {{ form.identificationNumber }}</h1>
|
<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 class="bg-primary-500 p-1 rounded-md flex items-center" title="Imprimer" @click="printReceipt">
|
||||||
|
<Icon name="mdi:printer-outline" size="32" class="cursor-pointer text-white"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingUsers"
|
:loading="isLoadingUsers"
|
||||||
wrapper-class="col-start-1 row-start-2"
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiDateInput
|
<UiDateInput
|
||||||
@@ -26,6 +29,7 @@
|
|||||||
v-model="form.shipmentDate"
|
v-model="form.shipmentDate"
|
||||||
label="Date d'expédition"
|
label="Date d'expédition"
|
||||||
wrapper-class="col-start-1 row-start-3"
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="col-start-1 row-start-4 h-[64px]">
|
<div class="col-start-1 row-start-4 h-[64px]">
|
||||||
@@ -41,6 +45,7 @@
|
|||||||
value: String(type.id),
|
value: String(type.id),
|
||||||
label: type.label
|
label: type.label
|
||||||
}))"
|
}))"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
id="shipment-type-quantity"
|
id="shipment-type-quantity"
|
||||||
@@ -63,6 +68,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingCustomers"
|
:loading="isLoadingCustomers"
|
||||||
wrapper-class="col-start-1 row-start-5"
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -72,6 +78,7 @@
|
|||||||
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||||
label="Adresse"
|
label="Adresse"
|
||||||
wrapper-class="col-start-2 row-start-1"
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -84,6 +91,7 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingTrucks"
|
:loading="isLoadingTrucks"
|
||||||
wrapper-class="col-start-2 row-start-2"
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UiSelect
|
<UiSelect
|
||||||
@@ -95,12 +103,14 @@
|
|||||||
label: carrier.name
|
label: carrier.name
|
||||||
}))"
|
}))"
|
||||||
wrapper-class="col-start-2 row-start-3"
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,6 +126,7 @@
|
|||||||
:loading="isLoadingVehicles"
|
:loading="isLoadingVehicles"
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
wrapper-class="col-start-2 row-start-4"
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="col-start-2 row-start-5 min-h-[72px]">
|
<div class="col-start-2 row-start-5 min-h-[72px]">
|
||||||
@@ -129,6 +140,7 @@
|
|||||||
label: driver.name
|
label: driver.name
|
||||||
}))"
|
}))"
|
||||||
:loading="isLoadingDrivers"
|
:loading="isLoadingDrivers"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,14 +149,20 @@
|
|||||||
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
|
||||||
<h1
|
<h1
|
||||||
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
|
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'"
|
:class="[
|
||||||
|
activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||||
|
hasGrossWeightError ? '!text-red-500 !border-red-500' : ''
|
||||||
|
]"
|
||||||
@click="activeTab = 'weights'"
|
@click="activeTab = 'weights'"
|
||||||
>
|
>
|
||||||
pesée à plein
|
pesée à plein
|
||||||
</h1>
|
</h1>
|
||||||
<h1
|
<h1
|
||||||
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
|
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'"
|
:class="[
|
||||||
|
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
|
||||||
|
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
|
||||||
|
]"
|
||||||
@click="activeTab = 'weightsEmpty'"
|
@click="activeTab = 'weightsEmpty'"
|
||||||
>
|
>
|
||||||
pesée à vide
|
pesée à vide
|
||||||
@@ -170,6 +188,7 @@
|
|||||||
<UiButton
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
@click="submitted = true"
|
||||||
>
|
>
|
||||||
Valider
|
Valider
|
||||||
</UiButton>
|
</UiButton>
|
||||||
@@ -220,6 +239,15 @@ const currentShipment = ref<ShipmentData | null>(null)
|
|||||||
const selectedShipmentTypeId = ref('')
|
const selectedShipmentTypeId = ref('')
|
||||||
const shipmentQuantity = ref<number | null>(0)
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
const allowAnyLicensePlate = ref(false)
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const submitted = ref(false)
|
||||||
|
|
||||||
|
const hasGrossWeightError = computed(() =>
|
||||||
|
submitted.value && (grossWeight.value.weight === null || grossWeight.value.weighedAt === null || grossWeight.value.dsd === null)
|
||||||
|
)
|
||||||
|
const hasTareWeightError = computed(() =>
|
||||||
|
submitted.value && (tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null)
|
||||||
|
)
|
||||||
|
|
||||||
const activeTab = ref<'weightsEmpty' | 'weights'>('weights')
|
const activeTab = ref<'weightsEmpty' | 'weights'>('weights')
|
||||||
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
|
||||||
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
|
||||||
@@ -376,7 +404,7 @@ function hydrateFromShipment(shipment: ShipmentData | null) {
|
|||||||
isHydrating.value = true
|
isHydrating.value = true
|
||||||
form.identificationNumber = shipment.identificationNumber ?? null
|
form.identificationNumber = shipment.identificationNumber ?? null
|
||||||
form.licensePlate = shipment.licensePlate ?? ''
|
form.licensePlate = shipment.licensePlate ?? ''
|
||||||
form.shipmentDate = shipment.shipmentDate ?? new Date().toISOString().slice(0, 10)
|
form.shipmentDate = shipment.shipmentDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
|
||||||
form.userId = shipment.user?.id ? String(shipment.user.id) : form.userId
|
form.userId = shipment.user?.id ? String(shipment.user.id) : form.userId
|
||||||
form.customerId = shipment.customer?.id ? String(shipment.customer.id) : ''
|
form.customerId = shipment.customer?.id ? String(shipment.customer.id) : ''
|
||||||
form.addressId = shipment.address?.id ? String(shipment.address.id) : ''
|
form.addressId = shipment.address?.id ? String(shipment.address.id) : ''
|
||||||
@@ -613,6 +641,14 @@ async function validate() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasInvalidWeights =
|
||||||
|
grossWeight.value.weight === null || grossWeight.value.weighedAt === null || grossWeight.value.dsd === null ||
|
||||||
|
tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null
|
||||||
|
|
||||||
|
if (hasInvalidWeights) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await updateShipment(shipmentId.value, {
|
await updateShipment(shipmentId.value, {
|
||||||
currentStep: currentShipment.value?.currentStep ?? 0,
|
currentStep: currentShipment.value?.currentStep ?? 0,
|
||||||
...buildPayload()
|
...buildPayload()
|
||||||
|
|||||||
@@ -1,73 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<WorkflowWaitingList
|
||||||
<div class="flex items-center gap-10">
|
title="listes des expéditions en attente"
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
:columns="columns"
|
||||||
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions en attente</h1>
|
:items="shipmentList ?? []"
|
||||||
</div>
|
route-prefix="/shipment"
|
||||||
</div>
|
show-actions
|
||||||
|
>
|
||||||
<div class="px-[86px]">
|
<template #cell-shipmentDate="{ item }">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
{{ formatDate(item.shipmentDate) }}
|
||||||
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
</template>
|
||||||
<div>Client</div>
|
<template #cell-shipmentType="{ item }">
|
||||||
<div>Adresse</div>
|
<template v-if="formatShipmentLines(item).length">
|
||||||
<div>Type d'expéditions</div>
|
<div
|
||||||
<div>Transporteur</div>
|
v-for="(line, index) in formatShipmentLines(item)"
|
||||||
<div>Immatriculation</div>
|
:key="index"
|
||||||
</div>
|
class="leading-5"
|
||||||
<div
|
>
|
||||||
v-for="shipment in shipmentList"
|
{{ line }}
|
||||||
: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>
|
||||||
<div>{{ shipment.carrier?.name }}</div>
|
</template>
|
||||||
<div>{{ shipment.licensePlate }}</div>
|
<template v-else>—</template>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<template #actions="{ item }">
|
||||||
</div>
|
<Icon
|
||||||
|
name="mdi:delete-outline"
|
||||||
|
size="24"
|
||||||
|
class="cursor-pointer text-red-500 hover:text-red-700"
|
||||||
|
@click="confirmDelete(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</WorkflowWaitingList>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ShipmentData } from '~/services/dto/shipment-data'
|
||||||
|
import { getShipmentList, deleteShipment } from '~/services/shipment'
|
||||||
|
|
||||||
import type {ShipmentData} from "~/services/dto/shipment-data";
|
const columns = [
|
||||||
import {getShipmentList} from "~/services/shipment";
|
{ key: 'shipmentDate', label: 'Date et heure' },
|
||||||
|
{ key: 'customer.name', label: 'Client' },
|
||||||
|
{ key: 'address.fullAddress', label: 'Adresse' },
|
||||||
|
{ key: 'shipmentType', label: "Type d'expé." },
|
||||||
|
{ key: 'carrier.name', label: 'Transporteur' },
|
||||||
|
{ key: 'licensePlate', label: 'Immatriculation' }
|
||||||
|
]
|
||||||
|
|
||||||
const shipmentList = ref<ShipmentData[]>()
|
const shipmentList = ref<ShipmentData[]>()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const goToShipment = (id: number) => {
|
const formatDate = (date: string | null) => {
|
||||||
router.push(`/shipment/${id}`)
|
if (!date) return '—'
|
||||||
|
const d = new Date(date.replace(' ', 'T'))
|
||||||
|
if (isNaN(d.getTime())) return date
|
||||||
|
return d.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatShipmentLines = (shipment: ShipmentData) => {
|
const formatShipmentLines = (shipment: ShipmentData) => {
|
||||||
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
|
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = typeof shipment.shipmentType === 'string'
|
const label = typeof shipment.shipmentType === 'string'
|
||||||
? shipment.shipmentType
|
? shipment.shipmentType
|
||||||
: shipment.shipmentType?.label
|
: shipment.shipmentType?.label
|
||||||
|
|
||||||
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
|
return [`${label ?? '—'} : ${shipment.nbBovinSend ?? '—'}`]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async (shipment: ShipmentData) => {
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
`Êtes-vous sûr de vouloir supprimer l'expédition ${shipment.identificationNumber ?? `#${shipment.id}`} ? Toutes les données liées seront supprimées.`
|
||||||
|
)
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
await deleteShipment(shipment.id)
|
||||||
|
shipmentList.value = shipmentList.value?.filter(s => s.id !== shipment.id)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
shipmentList.value = await getShipmentList(false)
|
shipmentList.value = await getShipmentList(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type { AddressData } from '~/services/dto/address-data'
|
|
||||||
export interface AddressPayload {
|
export interface AddressPayload {
|
||||||
label: string
|
|
||||||
street: string
|
street: string
|
||||||
street2?: string | null
|
street2?: string | null
|
||||||
postalCode: string
|
postalCode: string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export interface AddressData {
|
export interface AddressData {
|
||||||
id: number
|
id: number
|
||||||
label: string
|
label?: string | null
|
||||||
street: string
|
street: string
|
||||||
street2?: string | null
|
street2?: string | null
|
||||||
postalCode: string
|
postalCode: string
|
||||||
@@ -11,7 +11,7 @@ export interface AddressData {
|
|||||||
|
|
||||||
export interface AddressFormData {
|
export interface AddressFormData {
|
||||||
id?: number | null
|
id?: number | null
|
||||||
label: string
|
label?: string | null
|
||||||
street: string
|
street: string
|
||||||
street2?: string | null
|
street2?: string | null
|
||||||
postalCode: string
|
postalCode: string
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AddressFormData } from "~/services/dto/address-data"
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
import type { UserData } from "~/services/dto/user-data"
|
||||||
|
|
||||||
export type CustomerAddresses = AddressFormData[] | string[]
|
export type CustomerAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ export interface CustomerData {
|
|||||||
name: string
|
name: string
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
email?: string | null
|
email?: string | null
|
||||||
|
createdBy?: UserData | null
|
||||||
addresses: CustomerAddresses
|
addresses: CustomerAddresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AddressFormData } from "~/services/dto/address-data"
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
import type { UserData } from "~/services/dto/user-data"
|
||||||
|
|
||||||
export type SupplierAddresses = AddressFormData[] | string[]
|
export type SupplierAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ export interface SupplierData {
|
|||||||
name: string
|
name: string
|
||||||
email?: string | null
|
email?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
|
createdBy?: UserData | null
|
||||||
addresses: SupplierAddresses
|
addresses: SupplierAddresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
frontend/services/geo.ts
Normal file
16
frontend/services/geo.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export interface CommuneData {
|
||||||
|
nom: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCommunesByPostalCode(postalCode: string): Promise<CommuneData[]> {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const base = config.public.geoApiBase
|
||||||
|
try {
|
||||||
|
return await $fetch<CommuneData[]>(`${base}/communes`, {
|
||||||
|
params: { codePostal: postalCode, fields: 'nom', format: 'json' }
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,22 @@
|
|||||||
import {useApi} from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type {ReceptionData, ReceptionPayload} from '~/services/dto/reception-data'
|
import { createWorkflowService } from '~/services/workflow-service'
|
||||||
import type {WeightData} from '~/services/dto/weight-data'
|
import type { ReceptionData, ReceptionPayload } from '~/services/dto/reception-data'
|
||||||
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
export async function getReceptionList(isValid: boolean|null = null) {
|
const service = createWorkflowService<ReceptionData, ReceptionPayload>('receptions', 'reception')
|
||||||
|
|
||||||
|
export const getReceptionList = service.getList
|
||||||
|
export const getReception = service.get
|
||||||
|
export const createReception = service.create
|
||||||
|
export const updateReception = service.update
|
||||||
|
export const getWeight = service.getWeight as () => Promise<WeightData>
|
||||||
|
|
||||||
|
export async function deleteReception(id: number) {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const query = isValid !== null ? { isValid: isValid} : {}
|
return api.delete(`receptions/${id}`, {}, {
|
||||||
return api.get<ReceptionData[]>('receptions', query, {
|
toastSuccessKey: 'success.reception.delete',
|
||||||
toastErrorKey: 'errors.reception.list'
|
toastErrorKey: 'errors.reception.delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { service as receptionService }
|
||||||
export async function getReception(id: number) {
|
|
||||||
const api = useApi()
|
|
||||||
return api.get<ReceptionData>(`receptions/${id}`, {}, {
|
|
||||||
toastErrorKey: 'errors.reception.fetch'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createReception(payload: ReceptionPayload = {}) {
|
|
||||||
const api = useApi()
|
|
||||||
return api.post<ReceptionData>('receptions', payload, {
|
|
||||||
toastErrorKey: 'errors.reception.create'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateReception(id: number, payload: ReceptionPayload) {
|
|
||||||
const api = useApi()
|
|
||||||
return api.patch<ReceptionData>(`receptions/${id}`, payload, {
|
|
||||||
toastErrorKey: 'errors.reception.update',
|
|
||||||
toastSuccessKey: 'success.reception.update'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWeight(): Promise<WeightData> {
|
|
||||||
const api = useApi()
|
|
||||||
return api.get<WeightData>('receptions/weigh', {}, {
|
|
||||||
toastErrorKey: 'errors.reception.weigh'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,40 +1,22 @@
|
|||||||
import {useApi} from '~/composables/useApi'
|
import { createWorkflowService } from '~/services/workflow-service'
|
||||||
import type {ShipmentData, ShipmentPayload} from '~/services/dto/shipment-data'
|
import type { ShipmentData, ShipmentPayload } from '~/services/dto/shipment-data'
|
||||||
import type {WeightData} from '~/services/dto/weight-data'
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
export async function getShipmentList(isValid: boolean|null = null) {
|
const service = createWorkflowService<ShipmentData, ShipmentPayload>('shipments', 'shipment')
|
||||||
|
|
||||||
|
export const getShipmentList = service.getList
|
||||||
|
export const getShipment = service.get
|
||||||
|
export const createShipment = service.create
|
||||||
|
export const updateShipment = service.update
|
||||||
|
export const getWeightShipment = service.getWeight as () => Promise<WeightData>
|
||||||
|
|
||||||
|
export async function deleteShipment(id: number) {
|
||||||
|
const { useApi } = await import('~/composables/useApi')
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const query = isValid !== null ? { isValid: isValid} : {}
|
return api.delete(`shipments/${id}`, {}, {
|
||||||
return api.get<ShipmentData[]>('shipments', query, {
|
toastSuccessKey: 'success.shipment.delete',
|
||||||
toastErrorKey: 'errors.shipment.list'
|
toastErrorKey: 'errors.shipment.delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getShipment(id: number) {
|
export { service as shipmentService }
|
||||||
const api = useApi()
|
|
||||||
return api.get<ShipmentData>(`shipments/${id}`, {}, {
|
|
||||||
toastErrorKey: 'errors.shipment.fetch'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createShipment(payload: ShipmentPayload = {}) {
|
|
||||||
const api = useApi()
|
|
||||||
return api.post<ShipmentData>('shipments', payload, {
|
|
||||||
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'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
|
import type { 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
|
||||||
@@ -13,29 +10,15 @@ export type WeightPayload = {
|
|||||||
weighedAt: string | null
|
weighedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWeight(payload: WeightPayload) {
|
export async function createWeight(payload: WeightPayload | Record<string, any>) {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
return api.post<WeightEntryData>('weights', payload)
|
return api.post<WeightEntryData>('weights', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
toastErrorKey: 'errors.weight.update',
|
||||||
toastSuccessKey: 'success.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>
|
|
||||||
}
|
|
||||||
|
|||||||
55
frontend/services/workflow-service.ts
Normal file
55
frontend/services/workflow-service.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
export interface WorkflowService<TEntity, TPayload> {
|
||||||
|
getList: (isValid?: boolean | null) => Promise<TEntity[]>
|
||||||
|
get: (id: number) => Promise<TEntity>
|
||||||
|
create: (payload: TPayload) => Promise<TEntity>
|
||||||
|
update: (id: number, payload: TPayload) => Promise<TEntity>
|
||||||
|
getWeight: () => Promise<WeightData>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWorkflowService<TEntity, TPayload>(
|
||||||
|
apiResource: string,
|
||||||
|
toastPrefix: string
|
||||||
|
): WorkflowService<TEntity, TPayload> {
|
||||||
|
const getList = async (isValid: boolean | null = null): Promise<TEntity[]> => {
|
||||||
|
const api = useApi()
|
||||||
|
const query = isValid !== null ? { isValid } : {}
|
||||||
|
return api.get<TEntity[]>(apiResource, query, {
|
||||||
|
toastErrorKey: `errors.${toastPrefix}.list`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const get = async (id: number): Promise<TEntity> => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<TEntity>(`${apiResource}/${id}`, {}, {
|
||||||
|
toastErrorKey: `errors.${toastPrefix}.fetch`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const create = async (payload: TPayload): Promise<TEntity> => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<TEntity>(apiResource, payload, {
|
||||||
|
toastSuccessKey: `success.${toastPrefix}.create`,
|
||||||
|
toastErrorKey: `errors.${toastPrefix}.create`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = async (id: number, payload: TPayload): Promise<TEntity> => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<TEntity>(`${apiResource}/${id}`, payload, {
|
||||||
|
toastErrorKey: `errors.${toastPrefix}.update`,
|
||||||
|
toastSuccessKey: `success.${toastPrefix}.update`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWeight = async (): Promise<WeightData> => {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<WeightData>(`${apiResource}/weigh`, {}, {
|
||||||
|
toastErrorKey: `errors.${toastPrefix}.weigh`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getList, get, create, update, getWeight }
|
||||||
|
}
|
||||||
@@ -1,59 +1,19 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { useWorkflowStoreLogic } from '~/stores/workflow-store'
|
||||||
|
import { receptionService } from '~/services/reception'
|
||||||
import type { ReceptionData, ReceptionPayload } from '~/services/dto/reception-data'
|
import type { ReceptionData, ReceptionPayload } from '~/services/dto/reception-data'
|
||||||
import { createReception, getReception, updateReception } from '~/services/reception'
|
|
||||||
|
|
||||||
const isReceptionData = (value: unknown): value is ReceptionData => {
|
export const useReceptionStore = defineStore('reception', () => {
|
||||||
return Boolean(value && typeof value === 'object' && 'id' in value)
|
const { current, isLoading, setCurrent, clearCurrent, load, create, update } =
|
||||||
}
|
useWorkflowStoreLogic<ReceptionData, ReceptionPayload>(receptionService)
|
||||||
|
|
||||||
export const useReceptionStore = defineStore('reception', {
|
return {
|
||||||
state: () => ({
|
current,
|
||||||
current: null as ReceptionData | null,
|
isLoading,
|
||||||
isLoading: false
|
setCurrent,
|
||||||
}),
|
clearCurrent,
|
||||||
actions: {
|
loadReception: load,
|
||||||
setCurrent(reception: ReceptionData | null) {
|
createReception: create,
|
||||||
this.current = reception
|
updateReception: update
|
||||||
},
|
|
||||||
clearCurrent() {
|
|
||||||
this.current = null
|
|
||||||
},
|
|
||||||
async loadReception(id: number) {
|
|
||||||
this.isLoading = true
|
|
||||||
const result = await getReception(id).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
if (!isReceptionData(result)) {
|
|
||||||
this.current = null
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = result
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
async createReception(payload: ReceptionPayload = {}) {
|
|
||||||
this.isLoading = true
|
|
||||||
const result = await createReception(payload).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
if (!isReceptionData(result)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = result
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
async updateReception(id: number, payload: ReceptionPayload) {
|
|
||||||
this.isLoading = true
|
|
||||||
const result = await updateReception(id, payload).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
if (!isReceptionData(result)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.current = result
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,58 +1,19 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type {ShipmentData, ShipmentPayload} from "~/services/dto/shipment-data";
|
import { useWorkflowStoreLogic } from '~/stores/workflow-store'
|
||||||
import {createShipment, getShipment, updateShipment} from "~/services/shipment";
|
import { shipmentService } from '~/services/shipment'
|
||||||
|
import type { ShipmentData, ShipmentPayload } from '~/services/dto/shipment-data'
|
||||||
|
|
||||||
const isShipmentData = (value: unknown): value is ShipmentData => {
|
export const useShipmentStore = defineStore('shipment', () => {
|
||||||
return Boolean(value && typeof value === 'object' && 'id' in value)
|
const { current, isLoading, setCurrent, clearCurrent, load, create, update } =
|
||||||
}
|
useWorkflowStoreLogic<ShipmentData, ShipmentPayload>(shipmentService)
|
||||||
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 {
|
||||||
return result
|
current,
|
||||||
},
|
isLoading,
|
||||||
async createShipment(payload: ShipmentPayload = {}) {
|
setCurrent,
|
||||||
this.isLoading = true
|
clearCurrent,
|
||||||
const result = await createShipment(payload).finally(() => {
|
loadShipment: load,
|
||||||
this.isLoading = false
|
createShipment: create,
|
||||||
})
|
updateShipment: update
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
59
frontend/stores/workflow-store.ts
Normal file
59
frontend/stores/workflow-store.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { WorkflowService } from '~/services/workflow-service'
|
||||||
|
|
||||||
|
const isEntityData = (value: unknown): value is { id: number } => {
|
||||||
|
return Boolean(value && typeof value === 'object' && 'id' in value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorkflowStoreLogic<TEntity extends { id: number }, TPayload>(
|
||||||
|
service: WorkflowService<TEntity, TPayload>
|
||||||
|
) {
|
||||||
|
const current = ref<TEntity | null>(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const setCurrent = (entity: TEntity | null) => {
|
||||||
|
current.value = entity
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCurrent = () => {
|
||||||
|
current.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const load = async (id: number) => {
|
||||||
|
isLoading.value = true
|
||||||
|
const result = await service.get(id).finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
if (!isEntityData(result)) {
|
||||||
|
current.value = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
current.value = result as any
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const create = async (payload: TPayload) => {
|
||||||
|
isLoading.value = true
|
||||||
|
const result = await service.create(payload).finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
if (!isEntityData(result)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
current.value = result as any
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = async (id: number, payload: TPayload) => {
|
||||||
|
isLoading.value = true
|
||||||
|
const result = await service.update(id, payload).finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
if (!isEntityData(result)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
current.value = result as any
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return { current, isLoading, setCurrent, clearCurrent, load, create, update }
|
||||||
|
}
|
||||||
48
frontend/types/workflow.ts
Normal file
48
frontend/types/workflow.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { WeightEntryData } from '~/services/dto/reception-data'
|
||||||
|
import type { CarrierData } from '~/services/dto/carrier-data'
|
||||||
|
import type { TruckData } from '~/services/dto/truck-data'
|
||||||
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
|
import type { AddressData } from '~/services/dto/address-data'
|
||||||
|
import type { DriverData } from '~/services/dto/driver-data'
|
||||||
|
|
||||||
|
export interface WorkflowEntity {
|
||||||
|
id: number
|
||||||
|
identificationNumber?: string | null
|
||||||
|
licensePlate: string | null
|
||||||
|
currentStep: number
|
||||||
|
isValid: boolean
|
||||||
|
weights?: WeightEntryData[] | null
|
||||||
|
carrier?: CarrierData | null
|
||||||
|
truck?: TruckData | null
|
||||||
|
user?: UserData | null
|
||||||
|
address?: AddressData | null
|
||||||
|
driver?: DriverData | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowPayload {
|
||||||
|
licensePlate?: string | null
|
||||||
|
currentStep?: number
|
||||||
|
isValid?: boolean
|
||||||
|
carrier?: string | null
|
||||||
|
truck?: string | null
|
||||||
|
user?: string | null
|
||||||
|
address?: string | null
|
||||||
|
driver?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StepDefinition {
|
||||||
|
label: string
|
||||||
|
weighingMode?: 'gross' | 'tare'
|
||||||
|
isFinal?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowConfig {
|
||||||
|
entityName: 'reception' | 'shipment'
|
||||||
|
apiResource: string
|
||||||
|
steps: StepDefinition[]
|
||||||
|
weighingLabels: Record<'gross' | 'tare', string>
|
||||||
|
buildReceiptFilename: (entity: WorkflowEntity) => string
|
||||||
|
routePrefix: string
|
||||||
|
toastPrefix: string
|
||||||
|
dateField: string
|
||||||
|
}
|
||||||
33
migrations/Version20260318103644.php
Normal file
33
migrations/Version20260318103644.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?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 Version20260318103644 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 address ALTER label DROP NOT NULL');
|
||||||
|
$this->addSql('ALTER INDEX idx_ea9e2a42f8d859df RENAME TO IDX_2068337FF8D859DF');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE address ALTER label SET NOT NULL');
|
||||||
|
$this->addSql('ALTER INDEX idx_2068337ff8d859df RENAME TO idx_ea9e2a42f8d859df');
|
||||||
|
}
|
||||||
|
}
|
||||||
41
migrations/Version20260318104205.php
Normal file
41
migrations/Version20260318104205.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?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 Version20260318104205 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 customer ADD created_by INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD CONSTRAINT FK_81398E09DE12AB56 FOREIGN KEY (created_by) REFERENCES public."user" (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_81398E09DE12AB56 ON customer (created_by)');
|
||||||
|
$this->addSql('ALTER TABLE supplier ADD created_by INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE supplier ADD CONSTRAINT FK_9B2A6C7EDE12AB56 FOREIGN KEY (created_by) REFERENCES public."user" (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_9B2A6C7EDE12AB56 ON supplier (created_by)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE customer DROP CONSTRAINT FK_81398E09DE12AB56');
|
||||||
|
$this->addSql('DROP INDEX IDX_81398E09DE12AB56');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP created_by');
|
||||||
|
$this->addSql('ALTER TABLE supplier DROP CONSTRAINT FK_9B2A6C7EDE12AB56');
|
||||||
|
$this->addSql('DROP INDEX IDX_9B2A6C7EDE12AB56');
|
||||||
|
$this->addSql('ALTER TABLE supplier DROP created_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,9 +75,5 @@ chmod o+rx "$(dirname "$DEPLOY_DIR")" "$DEPLOY_DIR" 2>/dev/null || true
|
|||||||
|
|
||||||
echo "Release ${TAG} deployed to ${DEPLOY_DIR}"
|
echo "Release ${TAG} deployed to ${DEPLOY_DIR}"
|
||||||
|
|
||||||
if [ -f "${DEPLOY_DIR}/.env.local" ]; then
|
echo "Running migrations (if any)..."
|
||||||
echo "Running migrations (if any)..."
|
php "${DEPLOY_DIR}/bin/console" doctrine:migrations:migrate --no-interaction --env=prod
|
||||||
php "${DEPLOY_DIR}/bin/console" doctrine:migrations:migrate --no-interaction --env=prod
|
|
||||||
else
|
|
||||||
echo "Skip migrations: ${DEPLOY_DIR}/.env.local not found" >&2
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ class SeedCommand extends Command
|
|||||||
['label' => 'Bâtiment 1', 'code' => 'B1'],
|
['label' => 'Bâtiment 1', 'code' => 'B1'],
|
||||||
['label' => 'Bâtiment 2', 'code' => 'B2'],
|
['label' => 'Bâtiment 2', 'code' => 'B2'],
|
||||||
['label' => 'Bâtiment 3', 'code' => 'B3'],
|
['label' => 'Bâtiment 3', 'code' => 'B3'],
|
||||||
|
['label' => 'Zone tampon', 'code' => 'ZT'],
|
||||||
];
|
];
|
||||||
foreach ($buildings as $buildingData) {
|
foreach ($buildings as $buildingData) {
|
||||||
$this->upsertByCode(Building::class, $buildingData['code'], static function (Building $entity) use ($buildingData) {
|
$this->upsertByCode(Building::class, $buildingData['code'], static function (Building $entity) use ($buildingData) {
|
||||||
@@ -544,7 +545,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.49.20.09.10',
|
'phone' => '05.49.20.09.10',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'LIOT CHATELLERAULT',
|
|
||||||
'street' => "14 Allée d'Argenson",
|
'street' => "14 Allée d'Argenson",
|
||||||
'street2' => 'ZI Nord',
|
'street2' => 'ZI Nord',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -559,7 +559,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.49.02.65.27',
|
'phone' => '05.49.02.65.27',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'ARNAULT EURL',
|
|
||||||
'street' => 'Moulin du Guéret',
|
'street' => 'Moulin du Guéret',
|
||||||
'street2' => 'B.P 30425',
|
'street2' => 'B.P 30425',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -574,7 +573,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '06.80.14.18.82',
|
'phone' => '06.80.14.18.82',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'EARL DES GONNIERES',
|
|
||||||
'street' => "27 Route d'Ingrandes",
|
'street' => "27 Route d'Ingrandes",
|
||||||
'street2' => 'Les Gonnières',
|
'street2' => 'Les Gonnières',
|
||||||
'postalCode' => '86220',
|
'postalCode' => '86220',
|
||||||
@@ -589,7 +587,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.49.86.17.95',
|
'phone' => '05.49.86.17.95',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'EARL LESIGNY BABY',
|
|
||||||
'street' => '2 Lieu Dit Les Bouquins',
|
'street' => '2 Lieu Dit Les Bouquins',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86270',
|
'postalCode' => '86270',
|
||||||
@@ -604,7 +601,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '03.85.24.25.50',
|
'phone' => '03.85.24.25.50',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'FEDER',
|
|
||||||
'street' => 'Molaise',
|
'street' => 'Molaise',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '71120',
|
'postalCode' => '71120',
|
||||||
@@ -619,7 +615,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.49.86.57.24',
|
'phone' => '05.49.86.57.24',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => "GAEC DE L'ESPOIR",
|
|
||||||
'street' => 'La Moujonnerie',
|
'street' => 'La Moujonnerie',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86450',
|
'postalCode' => '86450',
|
||||||
@@ -634,7 +629,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.49.23.51.66',
|
'phone' => '05.49.23.51.66',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'GRAVELEAU',
|
|
||||||
'street' => '3, Le Jeu',
|
'street' => '3, Le Jeu',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86220',
|
'postalCode' => '86220',
|
||||||
@@ -649,7 +643,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.49.52.77.10',
|
'phone' => '05.49.52.77.10',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'LORTHOLARY',
|
|
||||||
'street' => 'Ferme de Geniec',
|
'street' => 'Ferme de Geniec',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86550',
|
'postalCode' => '86550',
|
||||||
@@ -664,7 +657,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.65.67.89.46',
|
'phone' => '05.65.67.89.46',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'NATERA',
|
|
||||||
'street' => 'Bd des Balquières',
|
'street' => 'Bd des Balquières',
|
||||||
'street2' => 'BP 3220',
|
'street2' => 'BP 3220',
|
||||||
'postalCode' => '12032',
|
'postalCode' => '12032',
|
||||||
@@ -674,12 +666,11 @@ class SeedCommand extends Command
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'SCEA des Bariollières',
|
'name' => 'SCEA DES BARIOLLIÈRES',
|
||||||
'email' => 'elisregnier@gmail.com',
|
'email' => 'elisregnier@gmail.com',
|
||||||
'phone' => '06.09.37.65.61',
|
'phone' => '06.09.37.65.61',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'SCEA des Bariollières',
|
|
||||||
'street' => '2 rue des Barriollières',
|
'street' => '2 rue des Barriollières',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86220',
|
'postalCode' => '86220',
|
||||||
@@ -694,7 +685,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => null,
|
'phone' => null,
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'SCEA SENE',
|
|
||||||
'street' => '3 Route de la Roche Posay',
|
'street' => '3 Route de la Roche Posay',
|
||||||
'street2' => 'Les Girouettes',
|
'street2' => 'Les Girouettes',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -709,7 +699,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '02.51.67.17.98',
|
'phone' => '02.51.67.17.98',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TERRENA',
|
|
||||||
'street' => 'La Blanchardière',
|
'street' => 'La Blanchardière',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '44522',
|
'postalCode' => '44522',
|
||||||
@@ -724,7 +713,6 @@ class SeedCommand extends Command
|
|||||||
'phone' => '05.49.19.44.33',
|
'phone' => '05.49.19.44.33',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TRICHERIE COOPERATIVE',
|
|
||||||
'street' => 'B.P n°2',
|
'street' => 'B.P n°2',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86490',
|
'postalCode' => '86490',
|
||||||
@@ -734,12 +722,11 @@ class SeedCommand extends Command
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'TURPAULT Muriel',
|
'name' => 'TURPAULT MURIEL',
|
||||||
'email' => null,
|
'email' => null,
|
||||||
'phone' => null,
|
'phone' => null,
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TURPAULT Muriel',
|
|
||||||
'street' => '23Bis Rue Marcel Pagnol',
|
'street' => '23Bis Rue Marcel Pagnol',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -748,6 +735,34 @@ class SeedCommand extends Command
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'EARL DE LA MENAUDIERE',
|
||||||
|
'email' => 'frederic.doussineau@orange.fr',
|
||||||
|
'phone' => '0675446004',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'street' => '1 la menaudière',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '86450',
|
||||||
|
'city' => 'LEIGNE LES BOIS ',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'SARL ERBS',
|
||||||
|
'email' => 'touillet.jacques@yahoo.fr',
|
||||||
|
'phone' => '0675030304',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'street' => 'les rodières ',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '86230',
|
||||||
|
'city' => 'Sérigny',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($suppliers as $supplierData) {
|
foreach ($suppliers as $supplierData) {
|
||||||
@@ -780,7 +795,6 @@ class SeedCommand extends Command
|
|||||||
'email' => 'eurl.arnault86@orange.fr',
|
'email' => 'eurl.arnault86@orange.fr',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'ARNAULT EURL',
|
|
||||||
'street' => 'Moulin du Guéret',
|
'street' => 'Moulin du Guéret',
|
||||||
'street2' => 'B.P 30425',
|
'street2' => 'B.P 30425',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -795,7 +809,6 @@ class SeedCommand extends Command
|
|||||||
'email' => 'sandra.robineaux@covilim.com',
|
'email' => 'sandra.robineaux@covilim.com',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'COVILIM',
|
|
||||||
'street' => 'Rue de Nexon',
|
'street' => 'Rue de Nexon',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '87000',
|
'postalCode' => '87000',
|
||||||
@@ -805,12 +818,11 @@ class SeedCommand extends Command
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'Les producteurs de la marche (LPM)',
|
'name' => 'LES PRODUCTEURS DE LA MARCHE (LPM)',
|
||||||
'phone' => '05.55.63.04.53',
|
'phone' => '05.55.63.04.53',
|
||||||
'email' => 'f.legalliard@lpmcoop.fr',
|
'email' => 'f.legalliard@lpmcoop.fr',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'Les producteurs de la marche (LPM)',
|
|
||||||
'street' => 'Malonze',
|
'street' => 'Malonze',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '23300',
|
'postalCode' => '23300',
|
||||||
@@ -825,7 +837,6 @@ class SeedCommand extends Command
|
|||||||
'email' => 'contact86@lortholarybetail.com',
|
'email' => 'contact86@lortholarybetail.com',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'LORTHOLARY BETAIL',
|
|
||||||
'street' => 'FERME DE GENIEC',
|
'street' => 'FERME DE GENIEC',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86550',
|
'postalCode' => '86550',
|
||||||
@@ -840,7 +851,6 @@ class SeedCommand extends Command
|
|||||||
'email' => 'scouillaud@terrena.fr',
|
'email' => 'scouillaud@terrena.fr',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TERRENA',
|
|
||||||
'street' => 'LA NOELLE',
|
'street' => 'LA NOELLE',
|
||||||
'street2' => 'BP 20199',
|
'street2' => 'BP 20199',
|
||||||
'postalCode' => '44155',
|
'postalCode' => '44155',
|
||||||
@@ -930,7 +940,7 @@ class SeedCommand extends Command
|
|||||||
{
|
{
|
||||||
$addressRepo = $this->entityManager->getRepository(Address::class);
|
$addressRepo = $this->entityManager->getRepository(Address::class);
|
||||||
$address = $addressRepo->findOneBy([
|
$address = $addressRepo->findOneBy([
|
||||||
'label' => $addressData['label'],
|
'street' => $addressData['street'],
|
||||||
'postalCode' => $addressData['postalCode'],
|
'postalCode' => $addressData['postalCode'],
|
||||||
]);
|
]);
|
||||||
if (!$address) {
|
if (!$address) {
|
||||||
@@ -940,7 +950,6 @@ class SeedCommand extends Command
|
|||||||
++$this->updated;
|
++$this->updated;
|
||||||
}
|
}
|
||||||
$address
|
$address
|
||||||
->setLabel($addressData['label'])
|
|
||||||
->setStreet($addressData['street'])
|
->setStreet($addressData['street'])
|
||||||
->setStreet2($addressData['street2'])
|
->setStreet2($addressData['street2'])
|
||||||
->setPostalCode($addressData['postalCode'])
|
->setPostalCode($addressData['postalCode'])
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class ReferenceFixtures extends Fixture
|
|||||||
['label' => 'Bâtiment 1', 'code' => 'B1'],
|
['label' => 'Bâtiment 1', 'code' => 'B1'],
|
||||||
['label' => 'Bâtiment 2', 'code' => 'B2'],
|
['label' => 'Bâtiment 2', 'code' => 'B2'],
|
||||||
['label' => 'Bâtiment 3', 'code' => 'B3'],
|
['label' => 'Bâtiment 3', 'code' => 'B3'],
|
||||||
|
['label' => 'Zone tampon', 'code' => 'ZT'],
|
||||||
];
|
];
|
||||||
foreach ($buildings as $buildingData) {
|
foreach ($buildings as $buildingData) {
|
||||||
$building = new Building()
|
$building = new Building()
|
||||||
@@ -107,7 +108,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.49.20.09.10',
|
'phone' => '05.49.20.09.10',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'LIOT CHATELLERAULT',
|
|
||||||
'street' => "14 Allée d'Argenson",
|
'street' => "14 Allée d'Argenson",
|
||||||
'street2' => 'ZI Nord',
|
'street2' => 'ZI Nord',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -122,7 +122,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.49.02.65.27',
|
'phone' => '05.49.02.65.27',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'ARNAULT EURL',
|
|
||||||
'street' => 'Moulin du Guéret',
|
'street' => 'Moulin du Guéret',
|
||||||
'street2' => 'B.P 30425',
|
'street2' => 'B.P 30425',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -137,7 +136,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '06.80.14.18.82',
|
'phone' => '06.80.14.18.82',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'EARL DES GONNIERES',
|
|
||||||
'street' => "27 Route d'Ingrandes",
|
'street' => "27 Route d'Ingrandes",
|
||||||
'street2' => 'Les Gonnières',
|
'street2' => 'Les Gonnières',
|
||||||
'postalCode' => '86220',
|
'postalCode' => '86220',
|
||||||
@@ -152,7 +150,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.49.86.17.95',
|
'phone' => '05.49.86.17.95',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'EARL LESIGNY BABY',
|
|
||||||
'street' => '2 Lieu Dit Les Bouquins',
|
'street' => '2 Lieu Dit Les Bouquins',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86270',
|
'postalCode' => '86270',
|
||||||
@@ -167,7 +164,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '03.85.24.25.50',
|
'phone' => '03.85.24.25.50',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'FEDER',
|
|
||||||
'street' => 'Molaise',
|
'street' => 'Molaise',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '71120',
|
'postalCode' => '71120',
|
||||||
@@ -182,7 +178,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.49.86.57.24',
|
'phone' => '05.49.86.57.24',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => "GAEC DE L'ESPOIR",
|
|
||||||
'street' => 'La Moujonnerie',
|
'street' => 'La Moujonnerie',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86450',
|
'postalCode' => '86450',
|
||||||
@@ -197,7 +192,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.49.23.51.66',
|
'phone' => '05.49.23.51.66',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'GRAVELEAU',
|
|
||||||
'street' => '3, Le Jeu',
|
'street' => '3, Le Jeu',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86220',
|
'postalCode' => '86220',
|
||||||
@@ -212,7 +206,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.49.52.77.10',
|
'phone' => '05.49.52.77.10',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'LORTHOLARY',
|
|
||||||
'street' => 'Ferme de Geniec',
|
'street' => 'Ferme de Geniec',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86550',
|
'postalCode' => '86550',
|
||||||
@@ -227,7 +220,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.65.67.89.46',
|
'phone' => '05.65.67.89.46',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'NATERA',
|
|
||||||
'street' => 'Bd des Balquières',
|
'street' => 'Bd des Balquières',
|
||||||
'street2' => 'BP 3220',
|
'street2' => 'BP 3220',
|
||||||
'postalCode' => '12032',
|
'postalCode' => '12032',
|
||||||
@@ -237,12 +229,11 @@ class ReferenceFixtures extends Fixture
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'SCEA des Bariollières',
|
'name' => 'SCEA DES BARIOLLIÈRES',
|
||||||
'email' => 'elisregnier@gmail.com',
|
'email' => 'elisregnier@gmail.com',
|
||||||
'phone' => '06.09.37.65.61',
|
'phone' => '06.09.37.65.61',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'SCEA des Bariollières',
|
|
||||||
'street' => '2 rue des Barriollières',
|
'street' => '2 rue des Barriollières',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86220',
|
'postalCode' => '86220',
|
||||||
@@ -257,7 +248,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => null,
|
'phone' => null,
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'SCEA SENE',
|
|
||||||
'street' => '3 Route de la Roche Posay',
|
'street' => '3 Route de la Roche Posay',
|
||||||
'street2' => 'Les Girouettes',
|
'street2' => 'Les Girouettes',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -272,7 +262,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '02.51.67.17.98',
|
'phone' => '02.51.67.17.98',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TERRENA',
|
|
||||||
'street' => 'La Blanchardière',
|
'street' => 'La Blanchardière',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '44522',
|
'postalCode' => '44522',
|
||||||
@@ -287,7 +276,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'phone' => '05.49.19.44.33',
|
'phone' => '05.49.19.44.33',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TRICHERIE COOPERATIVE',
|
|
||||||
'street' => 'B.P n°2',
|
'street' => 'B.P n°2',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86490',
|
'postalCode' => '86490',
|
||||||
@@ -297,12 +285,11 @@ class ReferenceFixtures extends Fixture
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'TURPAULT Muriel',
|
'name' => 'TURPAULT MURIEL',
|
||||||
'email' => null,
|
'email' => null,
|
||||||
'phone' => null,
|
'phone' => null,
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TURPAULT Muriel',
|
|
||||||
'street' => '23Bis Rue Marcel Pagnol',
|
'street' => '23Bis Rue Marcel Pagnol',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -311,6 +298,34 @@ class ReferenceFixtures extends Fixture
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'EARL DE LA MENAUDIERE',
|
||||||
|
'email' => 'frederic.doussineau@orange.fr',
|
||||||
|
'phone' => '0675446004',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'street' => '1 la menaudière',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '86450',
|
||||||
|
'city' => 'LEIGNE LES BOIS ',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'SARL ERBS',
|
||||||
|
'email' => 'touillet.jacques@yahoo.fr',
|
||||||
|
'phone' => '0675030304',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'street' => 'les rodières ',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '86230',
|
||||||
|
'city' => 'Sérigny',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($suppliers as $supplierData) {
|
foreach ($suppliers as $supplierData) {
|
||||||
@@ -321,10 +336,9 @@ class ReferenceFixtures extends Fixture
|
|||||||
;
|
;
|
||||||
|
|
||||||
foreach ($supplierData['addresses'] as $addressData) {
|
foreach ($supplierData['addresses'] as $addressData) {
|
||||||
$addressKey = sprintf('%s|%s', $addressData['label'], $addressData['postalCode']);
|
$addressKey = sprintf('%s|%s', $addressData['street'], $addressData['postalCode']);
|
||||||
if (!isset($addressIndex[$addressKey])) {
|
if (!isset($addressIndex[$addressKey])) {
|
||||||
$addressIndex[$addressKey] = new Address()
|
$addressIndex[$addressKey] = new Address()
|
||||||
->setLabel($addressData['label'])
|
|
||||||
->setStreet($addressData['street'])
|
->setStreet($addressData['street'])
|
||||||
->setStreet2($addressData['street2'])
|
->setStreet2($addressData['street2'])
|
||||||
->setPostalCode($addressData['postalCode'])
|
->setPostalCode($addressData['postalCode'])
|
||||||
@@ -347,7 +361,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'email' => 'eurl.arnault86@orange.fr',
|
'email' => 'eurl.arnault86@orange.fr',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'ARNAULT EURL',
|
|
||||||
'street' => 'Moulin du Guéret',
|
'street' => 'Moulin du Guéret',
|
||||||
'street2' => 'B.P 30425',
|
'street2' => 'B.P 30425',
|
||||||
'postalCode' => '86100',
|
'postalCode' => '86100',
|
||||||
@@ -362,7 +375,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'email' => 'sandra.robineaux@covilim.com',
|
'email' => 'sandra.robineaux@covilim.com',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'COVILIM',
|
|
||||||
'street' => 'Rue de Nexon',
|
'street' => 'Rue de Nexon',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '87000',
|
'postalCode' => '87000',
|
||||||
@@ -372,12 +384,11 @@ class ReferenceFixtures extends Fixture
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'Les producteurs de la marche (LPM)',
|
'name' => 'LES PRODUCTEURS DE LA MARCHE (LPM)',
|
||||||
'phone' => '05.55.63.04.53',
|
'phone' => '05.55.63.04.53',
|
||||||
'email' => 'f.legalliard@lpmcoop.fr',
|
'email' => 'f.legalliard@lpmcoop.fr',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'Les producteurs de la marche (LPM)',
|
|
||||||
'street' => 'Malonze',
|
'street' => 'Malonze',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '23300',
|
'postalCode' => '23300',
|
||||||
@@ -392,7 +403,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'email' => 'contact86@lortholarybetail.com',
|
'email' => 'contact86@lortholarybetail.com',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'LORTHOLARY BETAIL',
|
|
||||||
'street' => 'FERME DE GENIEC',
|
'street' => 'FERME DE GENIEC',
|
||||||
'street2' => null,
|
'street2' => null,
|
||||||
'postalCode' => '86550',
|
'postalCode' => '86550',
|
||||||
@@ -407,7 +417,6 @@ class ReferenceFixtures extends Fixture
|
|||||||
'email' => 'scouillaud@terrena.fr',
|
'email' => 'scouillaud@terrena.fr',
|
||||||
'addresses' => [
|
'addresses' => [
|
||||||
[
|
[
|
||||||
'label' => 'TERRENA',
|
|
||||||
'street' => 'LA NOELLE',
|
'street' => 'LA NOELLE',
|
||||||
'street2' => 'BP 20199',
|
'street2' => 'BP 20199',
|
||||||
'postalCode' => '44155',
|
'postalCode' => '44155',
|
||||||
@@ -426,10 +435,9 @@ class ReferenceFixtures extends Fixture
|
|||||||
;
|
;
|
||||||
|
|
||||||
foreach ($customerData['addresses'] as $addressData) {
|
foreach ($customerData['addresses'] as $addressData) {
|
||||||
$addressKey = sprintf('%s|%s', $addressData['label'], $addressData['postalCode']);
|
$addressKey = sprintf('%s|%s', $addressData['street'], $addressData['postalCode']);
|
||||||
if (!isset($addressIndex[$addressKey])) {
|
if (!isset($addressIndex[$addressKey])) {
|
||||||
$addressIndex[$addressKey] = new Address()
|
$addressIndex[$addressKey] = new Address()
|
||||||
->setLabel($addressData['label'])
|
|
||||||
->setStreet($addressData['street'])
|
->setStreet($addressData['street'])
|
||||||
->setStreet2($addressData['street2'])
|
->setStreet2($addressData['street2'])
|
||||||
->setPostalCode($addressData['postalCode'])
|
->setPostalCode($addressData['postalCode'])
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ class Address
|
|||||||
#[Groups(['address:read', 'supplier:read', 'customer:read', 'shipment:read'])]
|
#[Groups(['address:read', 'supplier:read', 'customer:read', 'shipment:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 120)]
|
#[ORM\Column(length: 120, nullable: true)]
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read'])]
|
||||||
private string $label = '';
|
private ?string $label = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
||||||
@@ -93,12 +93,12 @@ class Address
|
|||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabel(): string
|
public function getLabel(): ?string
|
||||||
{
|
{
|
||||||
return $this->label;
|
return $this->label;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setLabel(string $label): self
|
public function setLabel(?string $label): self
|
||||||
{
|
{
|
||||||
$this->label = $label;
|
$this->label = $label;
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class Carrier
|
|||||||
|
|
||||||
public function setName(string $name): self
|
public function setName(string $name): self
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = mb_strtoupper($name);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ class Customer
|
|||||||
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
private ?string $phone = null;
|
private ?string $phone = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
|
#[ORM\JoinColumn(name: 'created_by', nullable: true)]
|
||||||
|
#[Groups(['customer:read', 'customer:write'])]
|
||||||
|
private ?User $createdBy = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Address>
|
* @var Collection<int, Address>
|
||||||
*/
|
*/
|
||||||
@@ -85,7 +90,7 @@ class Customer
|
|||||||
|
|
||||||
public function setName(string $name): self
|
public function setName(string $name): self
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = mb_strtoupper($name);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -114,6 +119,18 @@ class Customer
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCreatedBy(): ?User
|
||||||
|
{
|
||||||
|
return $this->createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedBy(?User $createdBy): self
|
||||||
|
{
|
||||||
|
$this->createdBy = $createdBy;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getAddresses(): Collection
|
public function getAddresses(): Collection
|
||||||
{
|
{
|
||||||
return $this->addresses;
|
return $this->addresses;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
|||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiProperty;
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
use ApiPlatform\Metadata\Patch;
|
use ApiPlatform\Metadata\Patch;
|
||||||
@@ -30,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
#[ORM\Table(name: 'reception')]
|
#[ORM\Table(name: 'reception')]
|
||||||
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
|
order: ['id' => 'DESC'],
|
||||||
operations: [
|
operations: [
|
||||||
new Get(
|
new Get(
|
||||||
requirements: ['id' => '\d+'],
|
requirements: ['id' => '\d+'],
|
||||||
@@ -47,6 +49,9 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
normalizationContext: ['groups' => ['reception:read']],
|
normalizationContext: ['groups' => ['reception:read']],
|
||||||
denormalizationContext: ['groups' => ['reception:write']],
|
denormalizationContext: ['groups' => ['reception:write']],
|
||||||
),
|
),
|
||||||
|
new Delete(
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
),
|
||||||
new Get(
|
new Get(
|
||||||
uriTemplate: '/receptions/weigh',
|
uriTemplate: '/receptions/weigh',
|
||||||
openapi: new OpenApiOperation(
|
openapi: new OpenApiOperation(
|
||||||
@@ -96,7 +101,10 @@ class Reception
|
|||||||
|
|
||||||
#[ORM\Column(name: 'date_reception', type: 'datetime_immutable')]
|
#[ORM\Column(name: 'date_reception', type: 'datetime_immutable')]
|
||||||
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
|
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context(
|
||||||
|
normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i'],
|
||||||
|
denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'],
|
||||||
|
)]
|
||||||
private ?DateTimeImmutable $receptionDate = null;
|
private ?DateTimeImmutable $receptionDate = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
@@ -259,6 +267,14 @@ class Reception
|
|||||||
|
|
||||||
public function setReceptionDate(?DateTimeImmutable $receptionDate): self
|
public function setReceptionDate(?DateTimeImmutable $receptionDate): self
|
||||||
{
|
{
|
||||||
|
if (null !== $receptionDate && null !== $this->receptionDate) {
|
||||||
|
// Préserve l'heure existante quand seule la date est mise à jour
|
||||||
|
$receptionDate = $receptionDate->setTime(
|
||||||
|
(int) $this->receptionDate->format('H'),
|
||||||
|
(int) $this->receptionDate->format('i'),
|
||||||
|
(int) $this->receptionDate->format('s'),
|
||||||
|
);
|
||||||
|
}
|
||||||
$this->receptionDate = $receptionDate;
|
$this->receptionDate = $receptionDate;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
@@ -457,8 +473,16 @@ class Reception
|
|||||||
#[ORM\PrePersist]
|
#[ORM\PrePersist]
|
||||||
public function initializeReceptionDate(): void
|
public function initializeReceptionDate(): void
|
||||||
{
|
{
|
||||||
|
$now = new DateTimeImmutable();
|
||||||
if (null === $this->receptionDate) {
|
if (null === $this->receptionDate) {
|
||||||
$this->receptionDate = new DateTimeImmutable();
|
$this->receptionDate = $now;
|
||||||
|
} else {
|
||||||
|
// Préserve la date choisie mais utilise l'heure courante
|
||||||
|
$this->receptionDate = $this->receptionDate->setTime(
|
||||||
|
(int) $now->format('H'),
|
||||||
|
(int) $now->format('i'),
|
||||||
|
(int) $now->format('s'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
|||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiProperty;
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
use ApiPlatform\Metadata\Patch;
|
use ApiPlatform\Metadata\Patch;
|
||||||
@@ -30,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
#[ORM\Table(name: 'shipment')]
|
#[ORM\Table(name: 'shipment')]
|
||||||
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
|
order: ['id' => 'DESC'],
|
||||||
operations: [
|
operations: [
|
||||||
new Get(
|
new Get(
|
||||||
requirements: ['id' => '\d+'],
|
requirements: ['id' => '\d+'],
|
||||||
@@ -47,6 +49,9 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
normalizationContext: ['groups' => ['shipment:read']],
|
normalizationContext: ['groups' => ['shipment:read']],
|
||||||
denormalizationContext: ['groups' => ['shipment:write']],
|
denormalizationContext: ['groups' => ['shipment:write']],
|
||||||
),
|
),
|
||||||
|
new Delete(
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
),
|
||||||
new Get(
|
new Get(
|
||||||
uriTemplate: '/shipments/weigh',
|
uriTemplate: '/shipments/weigh',
|
||||||
openapi: new OpenApiOperation(
|
openapi: new OpenApiOperation(
|
||||||
@@ -102,7 +107,10 @@ class Shipment
|
|||||||
|
|
||||||
#[ORM\Column(name: 'shipment_date', type: 'datetime_immutable')]
|
#[ORM\Column(name: 'shipment_date', type: 'datetime_immutable')]
|
||||||
#[Groups(['shipment:read', 'shipment:write'])]
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context(
|
||||||
|
normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i'],
|
||||||
|
denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'],
|
||||||
|
)]
|
||||||
private ?DateTimeImmutable $shipmentDate = null;
|
private ?DateTimeImmutable $shipmentDate = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
@@ -245,6 +253,13 @@ class Shipment
|
|||||||
|
|
||||||
public function setShipmentDate(?DateTimeImmutable $shipmentDate): void
|
public function setShipmentDate(?DateTimeImmutable $shipmentDate): void
|
||||||
{
|
{
|
||||||
|
if (null !== $shipmentDate && null !== $this->shipmentDate) {
|
||||||
|
$shipmentDate = $shipmentDate->setTime(
|
||||||
|
(int) $this->shipmentDate->format('H'),
|
||||||
|
(int) $this->shipmentDate->format('i'),
|
||||||
|
(int) $this->shipmentDate->format('s'),
|
||||||
|
);
|
||||||
|
}
|
||||||
$this->shipmentDate = $shipmentDate;
|
$this->shipmentDate = $shipmentDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +318,21 @@ class Shipment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ORM\PrePersist]
|
||||||
|
public function initializeShipmentDate(): void
|
||||||
|
{
|
||||||
|
$now = new DateTimeImmutable();
|
||||||
|
if (null === $this->shipmentDate) {
|
||||||
|
$this->shipmentDate = $now;
|
||||||
|
} else {
|
||||||
|
$this->shipmentDate = $this->shipmentDate->setTime(
|
||||||
|
(int) $now->format('H'),
|
||||||
|
(int) $now->format('i'),
|
||||||
|
(int) $now->format('s'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[ORM\PostPersist]
|
#[ORM\PostPersist]
|
||||||
public function initializeIdentificationNumber(PostPersistEventArgs $args): void
|
public function initializeIdentificationNumber(PostPersistEventArgs $args): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ class Supplier
|
|||||||
#[Groups(['supplier:read', 'reception:read', 'supplier:write'])]
|
#[Groups(['supplier:read', 'reception:read', 'supplier:write'])]
|
||||||
private ?string $phone = null;
|
private ?string $phone = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
|
#[ORM\JoinColumn(name: 'created_by', nullable: true)]
|
||||||
|
#[Groups(['supplier:read', 'supplier:write'])]
|
||||||
|
private ?User $createdBy = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Address>
|
* @var Collection<int, Address>
|
||||||
*/
|
*/
|
||||||
@@ -87,7 +92,7 @@ class Supplier
|
|||||||
|
|
||||||
public function setName(string $name): self
|
public function setName(string $name): self
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = mb_strtoupper($name);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -116,6 +121,18 @@ class Supplier
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCreatedBy(): ?User
|
||||||
|
{
|
||||||
|
return $this->createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCreatedBy(?User $createdBy): self
|
||||||
|
{
|
||||||
|
$this->createdBy = $createdBy;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<int, Address>
|
* @return Collection<int, Address>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column(type: 'integer')]
|
#[ORM\Column(type: 'integer')]
|
||||||
#[Groups(['user:read', 'user-login:read', 'reception:read', 'shipment:read'])]
|
#[Groups(['user:read', 'user-login:read', 'reception:read', 'shipment:read', 'supplier:read', 'customer:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180, unique: true)]
|
#[ORM\Column(length: 180, unique: true)]
|
||||||
#[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read', 'shipment:read'])]
|
#[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read', 'shipment:read', 'supplier:read', 'customer:read'])]
|
||||||
private string $username = '';
|
private string $username = '';
|
||||||
|
|
||||||
#[ORM\Column(type: 'json')]
|
#[ORM\Column(type: 'json')]
|
||||||
|
|||||||
Reference in New Issue
Block a user