Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eee0745a7 | ||
| 845f94db8c | |||
|
|
86c0e74074 | ||
| be29daf4d1 | |||
|
|
08e7c1508c | ||
| 358da6a8ad | |||
|
|
67428186f6 | ||
| 09d108a1d5 | |||
|
|
f58dc36a0d | ||
| 15c0f414af | |||
|
|
9ed0ba702e | ||
| 93edd0a563 | |||
|
|
c361ef9bb9 | ||
| 7f3d9ef9c6 | |||
|
|
22b959de85 | ||
| d3bc2e11f1 | |||
|
|
d8b16f5e15 | ||
| 43213bc6d6 |
10
.idea/data_source_mapping.xml
generated
10
.idea/data_source_mapping.xml
generated
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DataSourcePerFileMappings">
|
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_1.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_2.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_3.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_4.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
216
.idea/workspace.xml
generated
216
.idea/workspace.xml
generated
@@ -4,28 +4,10 @@
|
|||||||
<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 : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : corrections diverses">
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/components/shipment/shipment-form.vue" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/data_source_mapping.xml" beforeDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/pages/shipment/[[id]].vue" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/bovin-shipment.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/customer.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/bovin-shipment-data.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/customer-data.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/shipment-data.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/shipment-type-data.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/shipment-type.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/services/shipment.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/frontend/stores/shipment.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.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$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/constants/steps.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/constants/steps.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/pages/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/index.vue" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/services/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/Entity/Address.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Address.php" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/Entity/BovinShipment.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/BovinShipment.php" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/Entity/Shipment.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Shipment.php" afterDir="false" />
|
|
||||||
</list>
|
</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" />
|
||||||
@@ -50,17 +32,21 @@
|
|||||||
<list>
|
<list>
|
||||||
<option value="Vue Composition API Component" />
|
<option value="Vue Composition API Component" />
|
||||||
<option value="TypeScript File" />
|
<option value="TypeScript File" />
|
||||||
|
<option value="PHP File" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="fix/makefile" />
|
<entry key="$PROJECT_DIR$" value="feat/276-lister-expeditions-terminees" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
</component>
|
||||||
<component name="McpProjectServerCommands">
|
<component name="McpProjectServerCommands">
|
||||||
<commands />
|
<commands />
|
||||||
<urls />
|
<urls />
|
||||||
@@ -237,37 +223,42 @@
|
|||||||
<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/271-expedition-etape-1",
|
"git-widget-placeholder": "fix/325-corrections-diverses",
|
||||||
"last_opened_file_path": "/home/sroy/Documents/test/Ferme",
|
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/kevin/Stage/Ferme/frontend/pages/shipment",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"settings.editor.selected.configurable": "configurable.tailwindcss",
|
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||||
"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">
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\pages\shipment" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\composables" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\components\shipment" />
|
||||||
|
</key>
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||||
@@ -313,54 +304,10 @@
|
|||||||
<workItem from="1770195718952" duration="215000" />
|
<workItem from="1770195718952" duration="215000" />
|
||||||
<workItem from="1770195959162" duration="18915000" />
|
<workItem from="1770195959162" duration="18915000" />
|
||||||
<workItem from="1770274844804" duration="3940000" />
|
<workItem from="1770274844804" duration="3940000" />
|
||||||
</task>
|
<workItem from="1770798536017" duration="20774000" />
|
||||||
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
|
<workItem from="1770879701502" duration="25805000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1770966186589" duration="914000" />
|
||||||
<created>1768237763998</created>
|
<workItem from="1770967274060" duration="2388000" />
|
||||||
<option name="number" value="00001" />
|
|
||||||
<option name="presentableId" value="LOCAL-00001" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768237763998</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00002" summary="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1768316052474</created>
|
|
||||||
<option name="number" value="00002" />
|
|
||||||
<option name="presentableId" value="LOCAL-00002" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768316052474</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00003" summary="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1768316835575</created>
|
|
||||||
<option name="number" value="00003" />
|
|
||||||
<option name="presentableId" value="LOCAL-00003" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768316835575</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00004" summary="feat : update du fichier AGENTS.md">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1768316965511</created>
|
|
||||||
<option name="number" value="00004" />
|
|
||||||
<option name="presentableId" value="LOCAL-00004" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768316965511</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1768317786187</created>
|
|
||||||
<option name="number" value="00005" />
|
|
||||||
<option name="presentableId" value="LOCAL-00005" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768317786187</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00006" summary="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1768318875533</created>
|
|
||||||
<option name="number" value="00006" />
|
|
||||||
<option name="presentableId" value="LOCAL-00006" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1768318875533</updated>
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
|
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -706,7 +653,55 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770217875423</updated>
|
<updated>1770217875423</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="50" />
|
<task id="LOCAL-00050" summary="feat : creer une nouvelle expedtion (WIP)">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770736570645</created>
|
||||||
|
<option name="number" value="00050" />
|
||||||
|
<option name="presentableId" value="LOCAL-00050" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770736570645</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00051" summary="feat : ajout d'une page de creation d'une expedition">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770880791564</created>
|
||||||
|
<option name="number" value="00051" />
|
||||||
|
<option name="presentableId" value="LOCAL-00051" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770880791565</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00052" summary="feat : changelog">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770881437439</created>
|
||||||
|
<option name="number" value="00052" />
|
||||||
|
<option name="presentableId" value="LOCAL-00052" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770881437439</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00053" summary="feat : lister les expeditions terminees">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770883114609</created>
|
||||||
|
<option name="number" value="00053" />
|
||||||
|
<option name="presentableId" value="LOCAL-00053" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770883114609</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00054" summary="feat : lister les expeditions terminees">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770884154297</created>
|
||||||
|
<option name="number" value="00054" />
|
||||||
|
<option name="presentableId" value="LOCAL-00054" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770884154297</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00055" summary="fix : corrections diverses">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1770969471135</created>
|
||||||
|
<option name="number" value="00055" />
|
||||||
|
<option name="presentableId" value="LOCAL-00055" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1770969471135</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="56" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -756,12 +751,6 @@
|
|||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="fix : correction du path URI pour la création d'un poids dans une réception" />
|
|
||||||
<MESSAGE value="feat : Ajout du bundle Monolog pour la gestion des logs" />
|
|
||||||
<MESSAGE value="fix : affiche plus détail dans les logs en recette/prod" />
|
|
||||||
<MESSAGE value="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod" />
|
|
||||||
<MESSAGE value="fix : doc de déploiement" />
|
|
||||||
<MESSAGE value="fix : doc et script de déploiement" />
|
|
||||||
<MESSAGE value="fix : gitea workflow" />
|
<MESSAGE value="fix : gitea workflow" />
|
||||||
<MESSAGE value="fix : script de déploiement" />
|
<MESSAGE value="fix : script de déploiement" />
|
||||||
<MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" />
|
<MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" />
|
||||||
@@ -781,7 +770,13 @@
|
|||||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||||
<MESSAGE value="feat : mise à jour du bon de réception" />
|
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||||
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||||
<option name="LAST_COMMIT_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 : ajout d'une page de creation d'une expedition" />
|
||||||
|
<MESSAGE value="feat : changelog" />
|
||||||
|
<MESSAGE value="feat : lister les expeditions terminees" />
|
||||||
|
<MESSAGE value="fix: corrections diverses" />
|
||||||
|
<MESSAGE value="fix : corrections diverses" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="fix : corrections diverses" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XDebuggerManager">
|
<component name="XDebuggerManager">
|
||||||
<breakpoint-manager>
|
<breakpoint-manager>
|
||||||
@@ -791,10 +786,19 @@
|
|||||||
<line>6</line>
|
<line>6</line>
|
||||||
<option name="timeStamp" value="3" />
|
<option name="timeStamp" value="3" />
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/Entity/Shipment.php</url>
|
||||||
|
<line>6</line>
|
||||||
|
<option name="timeStamp" value="45" />
|
||||||
|
</line-breakpoint>
|
||||||
<line-breakpoint enabled="true" type="javascript">
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
<url>file://$PROJECT_DIR$/frontend/services/shipment.ts</url>
|
<url>file://$PROJECT_DIR$/frontend/services/dto/shipment-data.ts</url>
|
||||||
<properties lambdaOrdinal="-1" />
|
<option name="timeStamp" value="43" />
|
||||||
<option name="timeStamp" value="37" />
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
|
<url>file://$PROJECT_DIR$/frontend/layouts/default.vue</url>
|
||||||
|
<line>72</line>
|
||||||
|
<option name="timeStamp" value="48" />
|
||||||
</line-breakpoint>
|
</line-breakpoint>
|
||||||
</breakpoints>
|
</breakpoints>
|
||||||
</breakpoint-manager>
|
</breakpoint-manager>
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* [#313] Admin modification creation fournisseur
|
* [#313] Admin modification creation fournisseur
|
||||||
* [#275] Lister les expéditions en attente
|
* [#275] Lister les expéditions en attente
|
||||||
* [#276] Lister les expéditions terminées
|
* [#276] Lister les expéditions terminées
|
||||||
|
* [#324] Creation page admin listing clients
|
||||||
|
* [#326] Admin modification creation client
|
||||||
|
* [#325] Correction diverses
|
||||||
|
* fix layout admin
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.0.41'
|
app.version: '0.0.50'
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink :to="link">
|
<NuxtLink :to="link">
|
||||||
<div class="w-[324px] h-[228px] border border-black rounded-md p-6 flex flex-col justify-between">
|
<div class="w-[300px] h-[216px] border border-black rounded-lg p-6 flex flex-col justify-between gap-4">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="rounded-full w-[80px] h-[80px] bg-neutral-400 flex justify-center items-center">
|
<div class="rounded-full w-[80px] h-[80px] bg-[#D9D9D9] flex justify-center items-center">
|
||||||
<Icon :name="iconName" style="color: black" size="44" />
|
<Icon :name="iconName" style="color: black" size="44" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uppercase font-bold">
|
<div class="uppercase font-bold">
|
||||||
<p class="text-3xl"> {{ label }} </p>
|
<p class="text-3xl text-primary-500">
|
||||||
|
<slot name="label">{{ label }}</slot>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -27,4 +27,3 @@ const props = defineProps<{
|
|||||||
label: string
|
label: string
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
|
||||||
class="flex flex-col items-center gap-16">
|
class="flex flex-col items-center gap-16">
|
||||||
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1>
|
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row gap-8 items-center">
|
class="flex flex-row gap-8 items-center">
|
||||||
<div
|
<div
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
:key="type.id"
|
:key="type.id"
|
||||||
class="mt-8 flex flex-row mb-2 gap-6">
|
class="mt-8 flex flex-row mb-2 gap-6">
|
||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
|
:id="type.id"
|
||||||
:label="type.label"
|
:label="type.label"
|
||||||
:code="type.code"
|
:code="type.code"
|
||||||
v-model="bovineQuantities[String(type.id)]"
|
v-model="bovineQuantities[String(type.id)]"
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
<button
|
<button
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
@click="goNext"
|
@click="goNext"
|
||||||
>Peser
|
>Valider
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -77,14 +78,14 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => receptionId.value,
|
[() => receptionId.value, () => bovineType.value],
|
||||||
async (id) => {
|
async ([id, types]) => {
|
||||||
if (!id || !receptionIri.value) {
|
if (!id || !receptionIri.value || types.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionMap: Record<string, number | null> = {}
|
const selectionMap: Record<string, number | null> = {}
|
||||||
for (const type of bovineType.value) {
|
for (const type of types) {
|
||||||
selectionMap[String(type.id)] = 0
|
selectionMap[String(type.id)] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
||||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Réception</h1>
|
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Réception</h1>
|
||||||
<!-- Nom de l'utilisateur -->
|
<!-- Nom de l'utilisateur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-user"
|
id="reception-user"
|
||||||
@@ -81,20 +81,8 @@
|
|||||||
select-class="h-[34px]"
|
select-class="h-[34px]"
|
||||||
wrapper-class="col-start-2 row-start-3"
|
wrapper-class="col-start-2 row-start-3"
|
||||||
/>
|
/>
|
||||||
<!-- Chauffeur (LIOT) -->
|
|
||||||
<UiSelect
|
|
||||||
id="reception-driver"
|
|
||||||
v-model="form.driverId"
|
|
||||||
label="Nom du chauffeur si LIOT"
|
|
||||||
:options="filteredDrivers.map((driver) => ({
|
|
||||||
value: String(driver.id),
|
|
||||||
label: driver.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingDrivers"
|
|
||||||
wrapper-class="col-start-2 row-start-4"
|
|
||||||
/>
|
|
||||||
<!-- Plaque d'immatriculation -->
|
<!-- Plaque d'immatriculation -->
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licensePlate"
|
v-model="form.licensePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
@@ -112,15 +100,28 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingVehicles"
|
:loading="isLoadingVehicles"
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4 h-[64px]"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
id="reception-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
|
v-if="isLiotCarrier"
|
||||||
wrapper-class="col-start-2 row-start-5"
|
wrapper-class="col-start-2 row-start-5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
>Peser
|
>Valider
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -342,7 +343,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
watch(
|
watch(
|
||||||
() => [form.supplierId, suppliers.value],
|
() => [form.supplierId, form.addressId, suppliers.value],
|
||||||
() => {
|
() => {
|
||||||
if (!form.supplierId) {
|
if (!form.supplierId) {
|
||||||
form.addressId = ''
|
form.addressId = ''
|
||||||
@@ -359,7 +360,11 @@ watch(
|
|||||||
(address) => String(address.id) === form.addressId
|
(address) => String(address.id) === form.addressId
|
||||||
)
|
)
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
form.addressId = ''
|
if (supplierAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(supplierAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{immediate: true}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
||||||
class="flex flex-col gap-16 items-center w-full">
|
class="flex flex-col gap-16 items-center w-full">
|
||||||
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1>
|
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des marchandises réceptionnnées</h1>
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="merchandise-type"
|
id="merchandise-type"
|
||||||
v-model="selectedMerchandiseTypeId"
|
v-model="selectedMerchandiseTypeId"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||||
class="flex gap-4 w-[550px] justify-evenly"
|
class="flex gap-4 w-[550px] justify-between"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="building in buildings"
|
v-for="building in buildings"
|
||||||
@@ -47,17 +47,17 @@
|
|||||||
>
|
>
|
||||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
||||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||||
<p class="font-bold uppercase">{{ type.label }}</p>
|
<p class="font-bold uppercase text-primary-500">{{ type.label }}</p>
|
||||||
<div
|
<div
|
||||||
v-for="building in buildings"
|
v-for="building in buildings"
|
||||||
:key="building.id"
|
:key="building.id"
|
||||||
class="flex items-center gap-2 text-lg"
|
class="flex items-center gap-2 text-lg pl-[2px]"
|
||||||
>
|
>
|
||||||
<UiCheckbox
|
<UiCheckbox
|
||||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||||
:value="String(building.id)"
|
:value="String(building.id)"
|
||||||
:label="building.label"
|
:label="building.label"
|
||||||
label-class="text-lg"
|
label-class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<button
|
<button
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
@click="goNext"
|
@click="goNext"
|
||||||
>Peser
|
>Valider
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1>
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
<p class="text-primary-500 uppercase text-2xl text-primary-500 mt-2">Pont-bascule connecté</p>
|
||||||
<div
|
<div
|
||||||
v-if="showLoadingBox"
|
v-if="showLoadingBox"
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
@@ -11,32 +11,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="displayWeight !== null" class="w-full">
|
<div v-else-if="displayWeight !== null" class="w-full">
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
|
||||||
{{ displayWeight }} kg
|
{{ displayWeight }} kg
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center mt-[54px]">
|
<div class="flex justify-center mt-[54px]">
|
||||||
<button
|
<UiButton
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
@click="fetchWeight"
|
@click="fetchWeight"
|
||||||
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="displayWeight !== null && !showGenerateReceipt"
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="saveWeight"
|
@click="saveWeight"
|
||||||
>Valider la pesée</button>
|
>Valider la pesée</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="showGenerateReceipt"
|
v-if="showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="printReceipt"
|
@click="printReceipt"
|
||||||
>Générer le bon</button>
|
>Générer le bon</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import {computed, onMounted} from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useWeighing } from '~/composables/useWeighing'
|
import { useWeighing } from '~/composables/useWeighing'
|
||||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
@@ -94,7 +94,7 @@ const printReceipt = async () => {
|
|||||||
|
|
||||||
// Récupère le poids dès l'arrivée sur l'écran
|
// Récupère le poids dès l'arrivée sur l'écran
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (false === displayWeight.value) {
|
if (displayWeight.value === null) {
|
||||||
fetchWeight()
|
fetchWeight()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,12 +27,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
>Valider
|
>Valider
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@@ -83,14 +83,14 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => receptionId,
|
[() => receptionId, () => bovineType.value],
|
||||||
async (id) => {
|
async ([id, types]) => {
|
||||||
if (!id || !receptionIri.value) {
|
if (!id || !receptionIri.value || types.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionMap: Record<string, number | null> = {}
|
const selectionMap: Record<string, number | null> = {}
|
||||||
for (const type of bovineType.value) {
|
for (const type of types) {
|
||||||
selectionMap[String(type.id)] = 0
|
selectionMap[String(type.id)] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ watch(
|
|||||||
}
|
}
|
||||||
Object.assign(bovineQuantities, selectionMap)
|
Object.assign(bovineQuantities, selectionMap)
|
||||||
|
|
||||||
const existingOther = await reception.bovineDetail
|
const existingOther = reception.bovineDetail
|
||||||
const parsedOther =
|
const parsedOther =
|
||||||
typeof existingOther === 'string' && existingOther.trim() !== ''
|
typeof existingOther === 'string' && existingOther.trim() !== ''
|
||||||
? Number(existingOther)
|
? Number(existingOther)
|
||||||
|
|||||||
@@ -1,79 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="flex flex-col items-center gap-16">
|
<div class="flex flex-col items-center gap-16">
|
||||||
<div
|
|
||||||
class="flex flex-col gap-16 items-center w-full">
|
|
||||||
<UiTextInput
|
|
||||||
id="merchandise-type"
|
|
||||||
v-model="selectedMerchandiseTypeId"
|
|
||||||
label="Type de marchandises"
|
|
||||||
:value="reception.merchandiseType?.label"
|
|
||||||
wrapper-class="w-[550px]"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
v-if="merchandiseTypeId && isAutres"
|
class="flex flex-col gap-16 items-center w-full">
|
||||||
class="flex flex-col w-full max-w-[550px]"
|
|
||||||
>
|
|
||||||
<UiTextInput
|
<UiTextInput
|
||||||
id="merchandise-detail"
|
id="merchandise-type"
|
||||||
:disabled="!auth.isAdmin"
|
v-model="selectedMerchandiseTypeId"
|
||||||
v-model="merchandiseDetail"
|
label="Type de marchandises"
|
||||||
label="Préciser"
|
:value="reception.merchandiseType?.label"
|
||||||
placeholder="Précisions complémentaires"
|
wrapper-class="w-[550px]"
|
||||||
:maxlength="255"
|
:disabled="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="merchandiseTypeId && !isGranule"
|
|
||||||
class="flex gap-4 w-[550px] justify-evenly"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="building in buildings"
|
v-if="merchandiseTypeId && isAutres"
|
||||||
:key="building.id"
|
class="flex flex-col w-full max-w-[550px]"
|
||||||
>
|
>
|
||||||
<UiCheckbox
|
<UiTextInput
|
||||||
v-model="selectedBuildingIds"
|
id="merchandise-detail"
|
||||||
:value="String(building.id)"
|
|
||||||
:label="building.label"
|
|
||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
label-class="text-xl"
|
v-model="merchandiseDetail"
|
||||||
|
label="Préciser"
|
||||||
|
placeholder="Précisions complémentaires"
|
||||||
|
:maxlength="255"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="merchandiseTypeId && isGranule"
|
v-if="merchandiseTypeId && !isGranule"
|
||||||
class="flex flex-col gap-10 w-full max-w-[1100px]"
|
class="flex gap-4 w-[550px] justify-evenly"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
<div
|
||||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
v-for="building in buildings"
|
||||||
<p class="font-bold uppercase">{{ type.label }}</p>
|
:key="building.id"
|
||||||
<div
|
>
|
||||||
v-for="building in buildings"
|
<UiCheckbox
|
||||||
:key="building.id"
|
v-model="selectedBuildingIds"
|
||||||
class="flex items-center gap-2 text-lg"
|
:value="String(building.id)"
|
||||||
>
|
:label="building.label"
|
||||||
<UiCheckbox
|
:disabled="!auth.isAdmin"
|
||||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
label-class="text-xl"
|
||||||
:value="String(building.id)"
|
/>
|
||||||
:label="building.label"
|
</div>
|
||||||
:disabled="!auth.isAdmin"
|
</div>
|
||||||
label-class="text-lg"
|
|
||||||
/>
|
<div
|
||||||
|
v-if="merchandiseTypeId && isGranule"
|
||||||
|
class="flex flex-col gap-10 w-full max-w-[1100px]"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
||||||
|
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||||
|
<p class="font-bold uppercase">{{ type.label }}</p>
|
||||||
|
<div
|
||||||
|
v-for="building in buildings"
|
||||||
|
:key="building.id"
|
||||||
|
class="flex items-center gap-2 text-lg"
|
||||||
|
>
|
||||||
|
<UiCheckbox
|
||||||
|
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||||
|
:value="String(building.id)"
|
||||||
|
:label="building.label"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
label-class="text-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<UiButton
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
type="submit"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
>Valider
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
|
||||||
:disabled="!auth.isAdmin"
|
|
||||||
>Valider
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
|
<div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-8">
|
||||||
<div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-16">
|
|
||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
label="Pesée à vide"
|
label="Dsd"
|
||||||
v-model="form.weights[0].weight"
|
class="col-start-2"
|
||||||
|
labelClass="font-bold uppercase"
|
||||||
|
v-model="sharedWeightMeta.dsd"
|
||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
:min="0"
|
|
||||||
/>
|
/>
|
||||||
|
<UiDateInput
|
||||||
|
label="Date pesée"
|
||||||
|
v-model="sharedWeightMeta.weighedAt"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-x-40 mb-16">
|
||||||
<UiNumberInput
|
<UiNumberInput
|
||||||
label="Pesée à plein"
|
v-for="weight in form.weights"
|
||||||
v-model="form.weights[1].weight"
|
:key="weight.type"
|
||||||
|
:label="getWeightLabel(weight.type)"
|
||||||
|
labelClass="font-bold uppercase text-xl"
|
||||||
|
inputClass="w-24"
|
||||||
|
v-model="weight.weight"
|
||||||
|
:wrapper-class="weight.type === 'tare' ? 'col-start-1 row-start-1' : 'col-start-2 row-start-1'"
|
||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
:min="0"
|
:min="0"
|
||||||
|
:max="48000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button
|
<UiButton
|
||||||
|
v-if="auth.isAdmin"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
:disabled="!auth.isAdmin"
|
|
||||||
>
|
>
|
||||||
Valider
|
Valider
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
@@ -32,7 +45,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {ReceptionFormWeight} from '~/services/dto/reception-data'
|
import type {ReceptionFormWeight} from '~/services/dto/reception-data'
|
||||||
import { getReception } from '~/services/reception'
|
import {getReception} from '~/services/reception'
|
||||||
import {updateWeight} from "~/services/weight";
|
import {updateWeight} from "~/services/weight";
|
||||||
import {useAuthStore} from "~/stores/auth";
|
import {useAuthStore} from "~/stores/auth";
|
||||||
|
|
||||||
@@ -45,17 +58,42 @@ const auth = useAuthStore()
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
weights: [
|
weights: [
|
||||||
{ id: 0, type: 'tare' as const, weight: 0 },
|
{id: 0, type: 'tare' as const, weight: 0, dsd: null, weighedAt: null},
|
||||||
{ id: 0, type: 'gross' as const, weight: 0 }
|
{id: 0, type: 'gross' as const, weight: 0, dsd: null, weighedAt: null}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
// DSD et date de pesée sont partagés entre tare et gross dans l'UI.
|
||||||
|
const sharedWeightMeta = reactive<{
|
||||||
|
dsd: number | string | null
|
||||||
|
weighedAt: string | null
|
||||||
|
}>({
|
||||||
|
dsd: null,
|
||||||
|
weighedAt: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const getWeightLabel = (type: 'tare' | 'gross'): string => {
|
||||||
|
return type === 'tare' ? 'Pesée à vide' : 'Pesée à plein'
|
||||||
|
}
|
||||||
|
|
||||||
const hydrateFromReception = (reception: ReceptionFormWeight) => {
|
const hydrateFromReception = (reception: ReceptionFormWeight) => {
|
||||||
const tare = reception.weights.find(weight => weight.type === 'tare')
|
// On hydrate chaque ligne par son type (tare/gross), sans dépendre d'un index.
|
||||||
const gross = reception.weights.find(weight => weight.type === 'gross')
|
for (const receptionWeight of reception.weights) {
|
||||||
|
const formWeight = form.weights.find(weight => weight.type === receptionWeight.type)
|
||||||
|
if (formWeight) {
|
||||||
|
Object.assign(formWeight, receptionWeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tare) form.weights[0] = { ...tare }
|
// On récupère une valeur existante pour préremplir les champs partagés.
|
||||||
if (gross) form.weights[1] = { ...gross }
|
const weightWithMeta = reception.weights.find(weight =>
|
||||||
|
(weight.dsd !== null && weight.dsd !== undefined)
|
||||||
|
|| (weight.weighedAt !== null && weight.weighedAt !== undefined && weight.weighedAt !== '')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (weightWithMeta) {
|
||||||
|
sharedWeightMeta.dsd = weightWithMeta.dsd ?? null
|
||||||
|
sharedWeightMeta.weighedAt = weightWithMeta.weighedAt ?? null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -64,11 +102,23 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function validate() {
|
async function validate() {
|
||||||
|
const sharedDsd =
|
||||||
|
sharedWeightMeta.dsd === null || sharedWeightMeta.dsd === undefined || sharedWeightMeta.dsd === ''
|
||||||
|
? null
|
||||||
|
: Number(sharedWeightMeta.dsd)
|
||||||
|
const sharedWeighedAt =
|
||||||
|
sharedWeightMeta.weighedAt === null || sharedWeightMeta.weighedAt === undefined || sharedWeightMeta.weighedAt === ''
|
||||||
|
? null
|
||||||
|
: sharedWeightMeta.weighedAt
|
||||||
for (const weight of form.weights) {
|
for (const weight of form.weights) {
|
||||||
if (weight.id) {
|
if (weight.id) {
|
||||||
await updateWeight(weight.id, {weight: weight.weight})
|
await updateWeight(weight.id, {
|
||||||
|
weight: weight.weight,
|
||||||
|
dsd: Number.isFinite(sharedDsd) ? sharedDsd : null,
|
||||||
|
weighedAt: sharedWeighedAt
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
||||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">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 -->
|
<!-- Nom de l'utilisateur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="shipment-user"
|
id="shipment-user"
|
||||||
@@ -22,24 +22,27 @@
|
|||||||
wrapper-class="col-start-1 row-start-3"
|
wrapper-class="col-start-1 row-start-3"
|
||||||
/>
|
/>
|
||||||
<!-- Type d'expédition -->
|
<!-- Type d'expédition -->
|
||||||
<div class="col-start-1 row-start-4">
|
<div class="col-start-1 row-start-4 h-[64px]">
|
||||||
<label class="font-bold uppercase text-xl mb-2 block">
|
<div class="flex items-end gap-8 justify-between">
|
||||||
Type d'expédition
|
<UiRadioGroup
|
||||||
</label>
|
id="shipment-type"
|
||||||
<div class="grid grid-cols-2 gap-x-8">
|
name="shipment-type"
|
||||||
<div
|
label="Type d'expédition bovine"
|
||||||
v-for="type in bovineShipment"
|
v-model="selectedShipmentTypeId"
|
||||||
:key="type.id"
|
:options="bovineShipment.map((type) => ({
|
||||||
class="mt-8 flex flex-row gap-6"
|
value: String(type.id),
|
||||||
>
|
label: type.label
|
||||||
<UiNumberInput
|
}))"
|
||||||
:label="type.label"
|
/>
|
||||||
v-model="bovineQuantities[String(type.id)]"
|
<UiNumberInput
|
||||||
:placeholder="0"
|
id="shipment-type-quantity"
|
||||||
:min="0"
|
label="Quantité"
|
||||||
:max="10"
|
v-model="shipmentQuantity"
|
||||||
/>
|
:placeholder="0"
|
||||||
</div>
|
:min="0"
|
||||||
|
:max="1200"
|
||||||
|
:disabled="!selectedShipmentTypeId"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Client -->
|
<!-- Client -->
|
||||||
@@ -49,7 +52,7 @@
|
|||||||
label="Client"
|
label="Client"
|
||||||
:options="customers.map((customer) => ({
|
:options="customers.map((customer) => ({
|
||||||
value: String(customer.id),
|
value: String(customer.id),
|
||||||
label: customer.label
|
label: customer.name || `Client #${customer.id}`
|
||||||
}))"
|
}))"
|
||||||
:loading="isLoadingCustomers"
|
:loading="isLoadingCustomers"
|
||||||
wrapper-class="col-start-1 row-start-5"
|
wrapper-class="col-start-1 row-start-5"
|
||||||
@@ -86,20 +89,8 @@
|
|||||||
}))"
|
}))"
|
||||||
wrapper-class="col-start-2 row-start-3"
|
wrapper-class="col-start-2 row-start-3"
|
||||||
/>
|
/>
|
||||||
<!-- Chauffeur (LIOT) -->
|
|
||||||
<UiSelect
|
|
||||||
id="shipment-driver"
|
|
||||||
v-model="form.driverId"
|
|
||||||
label="Nom du chauffeur si LIOT"
|
|
||||||
:options="filteredDrivers.map((driver) => ({
|
|
||||||
value: String(driver.id),
|
|
||||||
label: driver.name
|
|
||||||
}))"
|
|
||||||
:loading="isLoadingDrivers"
|
|
||||||
wrapper-class="col-start-2 row-start-4"
|
|
||||||
/>
|
|
||||||
<!-- Plaque d'immatriculation (hors LIOT) -->
|
<!-- Plaque d'immatriculation (hors LIOT) -->
|
||||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
|
||||||
<UiLicensePlateInput
|
<UiLicensePlateInput
|
||||||
v-model="form.licencePlate"
|
v-model="form.licencePlate"
|
||||||
v-model:allowAny="allowAnyLicensePlate"
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
@@ -117,15 +108,28 @@
|
|||||||
}))"
|
}))"
|
||||||
:loading="isLoadingVehicles"
|
:loading="isLoadingVehicles"
|
||||||
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-4"
|
||||||
|
/>
|
||||||
|
<!-- Chauffeur (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-driver"
|
||||||
|
v-model="form.driverId"
|
||||||
|
label="Nom du chauffeur si LIOT"
|
||||||
|
:options="filteredDrivers.map((driver) => ({
|
||||||
|
value: String(driver.id),
|
||||||
|
label: driver.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingDrivers"
|
||||||
wrapper-class="col-start-2 row-start-5"
|
wrapper-class="col-start-2 row-start-5"
|
||||||
|
v-if="isLiotCarrier"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
>Valider
|
>Valider
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@@ -177,8 +181,9 @@ const isLoadingDrivers = ref(false)
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const shipmentStore = useShipmentStore()
|
const shipmentStore = useShipmentStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const bovineQuantities = ref<Record<string, number | null>>({})
|
|
||||||
const bovineShipment = ref<ShipmentTypeData[]>([])
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
|
const selectedShipmentTypeId = ref('')
|
||||||
|
const shipmentQuantity = ref<number | null>(0)
|
||||||
// Transporteur sélectionné dans le formulaire
|
// Transporteur sélectionné dans le formulaire
|
||||||
const selectedCarrier = computed(() =>
|
const selectedCarrier = computed(() =>
|
||||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
@@ -328,15 +333,21 @@ watch(
|
|||||||
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) : ''
|
||||||
if (!shipment || !shipment.bovinShipments) {
|
if (!shipment || !shipment.bovinShipments) {
|
||||||
bovineQuantities.value = {}
|
selectedShipmentTypeId.value = ''
|
||||||
|
shipmentQuantity.value = 0
|
||||||
} else {
|
} else {
|
||||||
const next: Record<string, number | null> = {}
|
const selectedEntry = shipment.bovinShipments.find((entry) => {
|
||||||
for (const entry of shipment.bovinShipments) {
|
|
||||||
const typeId = entry.shipmentType?.id
|
const typeId = entry.shipmentType?.id
|
||||||
if (!typeId) continue
|
return Boolean(typeId) && Number(entry.nbBovinSend ?? 0) > 0
|
||||||
next[String(typeId)] = entry.nbBovinSend ?? null
|
}) ?? shipment.bovinShipments.find((entry) => Boolean(entry.shipmentType?.id))
|
||||||
|
|
||||||
|
if (!selectedEntry?.shipmentType?.id) {
|
||||||
|
selectedShipmentTypeId.value = ''
|
||||||
|
shipmentQuantity.value = 0
|
||||||
|
} else {
|
||||||
|
selectedShipmentTypeId.value = String(selectedEntry.shipmentType.id)
|
||||||
|
shipmentQuantity.value = selectedEntry.nbBovinSend ?? 0
|
||||||
}
|
}
|
||||||
bovineQuantities.value = next
|
|
||||||
}
|
}
|
||||||
isHydrating.value = false
|
isHydrating.value = false
|
||||||
},
|
},
|
||||||
@@ -344,7 +355,7 @@ watch(
|
|||||||
)
|
)
|
||||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
watch(
|
watch(
|
||||||
() => [form.customerId, customers.value],
|
() => [form.customerId, form.addressId, customers.value],
|
||||||
() => {
|
() => {
|
||||||
if (!form.customerId) {
|
if (!form.customerId) {
|
||||||
form.addressId = ''
|
form.addressId = ''
|
||||||
@@ -361,7 +372,11 @@ watch(
|
|||||||
(address) => String(address.id) === form.addressId
|
(address) => String(address.id) === form.addressId
|
||||||
)
|
)
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
form.addressId = ''
|
if (customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{immediate: true}
|
||||||
@@ -460,16 +475,22 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
const buildDesiredBovinShipments = () => {
|
const buildDesiredBovinShipments = () => {
|
||||||
return bovineShipment.value
|
const typeId = Number(selectedShipmentTypeId.value)
|
||||||
.map((type) => {
|
if (!Number.isFinite(typeId)) {
|
||||||
const raw = bovineQuantities.value[String(type.id)]
|
return []
|
||||||
const quantity = raw === null || raw === undefined ? 0 : Number(raw)
|
}
|
||||||
return {
|
const type = bovineShipment.value.find((entry) => entry.id === typeId)
|
||||||
type,
|
if (!type) {
|
||||||
quantity: Number.isFinite(quantity) ? Math.max(0, Math.trunc(quantity)) : 0
|
return []
|
||||||
}
|
}
|
||||||
})
|
const raw = shipmentQuantity.value
|
||||||
.filter((entry) => entry.quantity > 0)
|
const quantity = raw === null || raw === undefined ? 0 : Number(raw)
|
||||||
|
const normalizedQuantity = Number.isFinite(quantity) ? Math.max(0, Math.trunc(quantity)) : 0
|
||||||
|
if (normalizedQuantity <= 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{type, quantity: normalizedQuantity}]
|
||||||
}
|
}
|
||||||
const syncBovinShipments = async (
|
const syncBovinShipments = async (
|
||||||
shipmentId: number,
|
shipmentId: number,
|
||||||
|
|||||||
26
frontend/components/shipment/shipment-loading.vue
Normal file
26
frontend/components/shipment/shipment-loading.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center gap-[118px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase text-primary-500">Charment des bovins</h1>
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="goNext"
|
||||||
|
>Peser</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useShipmentStore} from "~/stores/shipment";
|
||||||
|
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
|
||||||
|
const goNext = async () => {
|
||||||
|
const nextStep = shipmentStore.current.currentStep + 1
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: nextStep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1>
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
||||||
<div
|
<div
|
||||||
@@ -11,27 +11,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="displayWeight !== null" class="w-full">
|
<div v-else-if="displayWeight !== null" class="w-full">
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
class="w-full flex flex-col items-center justify-center border border-primary-500 h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
|
||||||
{{ displayWeight }} kg
|
{{ displayWeight }} kg
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center mt-[54px]">
|
<div class="flex justify-center mt-[54px]">
|
||||||
<button
|
<UiButton
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
@click="fetchWeight"
|
@click="fetchWeight"
|
||||||
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="displayWeight !== null && !showGenerateReceipt"
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="saveWeight"
|
@click="saveWeight"
|
||||||
>Valider la pesée</button>
|
>Valider la pesée</UiButton>
|
||||||
<button
|
<UiButton
|
||||||
v-if="showGenerateReceipt"
|
v-if="showGenerateReceipt"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
@click="printReceipt"
|
@click="printReceipt"
|
||||||
>Générer le bon</button>
|
>Générer le bon</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
39
frontend/components/ui/UiButton.vue
Normal file
39
frontend/components/ui/UiButton.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="'button'"
|
||||||
|
:type="type"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
class="inline-flex items-center justify-center rounded-md"
|
||||||
|
:class="[
|
||||||
|
isDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
|
||||||
|
buttonClass
|
||||||
|
]"
|
||||||
|
v-bind="attrs"
|
||||||
|
>
|
||||||
|
<slot v-if="!loading" />
|
||||||
|
<UiLoadingDots v-else />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, useAttrs} from 'vue'
|
||||||
|
|
||||||
|
defineOptions({inheritAttrs: false})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
type?: 'button' | 'submit' | 'reset'
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
buttonClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
buttonClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const isDisabled = computed(() => props.disabled || props.loading)
|
||||||
|
</script>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<label
|
<label
|
||||||
class="flex items-center gap-2"
|
class="flex items-center gap-2 cursor-pointer text-primary-500"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="checked"
|
:checked="checked"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="inputClass"
|
:class="['cursor-pointer text-primary-500', inputClass]"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<span v-if="label">{{ label }}</span>
|
<span v-if="label">{{ label }}</span>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="font-bold uppercase text-xl mb-2"
|
class="font-bold uppercase text-xl text-primary-500"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
:value="modelValue ?? ''"
|
:value="modelValue ?? ''"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
class="border-b border-black justify-self-start text-xl text-primary-500 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="text-xl text-bold flex items-center"
|
class="text-xl flex items-center gap-2 text-primary-500"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
:step="step"
|
:step="step"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black text-xl bg-transparent w-12"
|
class="border-b border-black text-xl bg-transparent w-16 text-primary-500"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||||
@@ -74,14 +74,41 @@ const emit = defineEmits<{
|
|||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
const isEmpty = computed(() => props.modelValue === null || props.modelValue === undefined || props.modelValue === '')
|
const isEmpty = computed(() => props.modelValue === null || props.modelValue === undefined || props.modelValue === '')
|
||||||
|
|
||||||
|
const toNumberOrNull = (value: number | string | undefined) => {
|
||||||
|
if (value === undefined || value === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
|
}
|
||||||
|
|
||||||
const onInput = (event: Event) => {
|
const onInput = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
if (target.value === '') {
|
if (target.value === '') {
|
||||||
emit('update:modelValue', null)
|
emit('update:modelValue', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const numeric = Math.max(0, Number(target.value))
|
const parsed = Number(target.value)
|
||||||
emit('update:modelValue', Number.isNaN(numeric) ? null : numeric)
|
if (!Number.isFinite(parsed)) {
|
||||||
|
emit('update:modelValue', null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = toNumberOrNull(props.min)
|
||||||
|
const max = toNumberOrNull(props.max)
|
||||||
|
|
||||||
|
let numeric = parsed
|
||||||
|
if (min !== null) {
|
||||||
|
numeric = Math.max(min, numeric)
|
||||||
|
} else {
|
||||||
|
numeric = Math.max(0, numeric)
|
||||||
|
}
|
||||||
|
if (max !== null) {
|
||||||
|
numeric = Math.min(max, numeric)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.value = String(numeric)
|
||||||
|
emit('update:modelValue', numeric)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeydown = (event: KeyboardEvent) => {
|
const onKeydown = (event: KeyboardEvent) => {
|
||||||
|
|||||||
93
frontend/components/ui/UiRadioGroup.vue
Normal file
93
frontend/components/ui/UiRadioGroup.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['flex flex-col', wrapperClass]">
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
class="font-bold uppercase text-xl text-primary-500"
|
||||||
|
:class="labelClass"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
:aria-label="label || id || 'radio-group'"
|
||||||
|
:class="['flex items-center gap-6 mt-1', groupClass]"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-for="option in options"
|
||||||
|
:key="String(option.value)"
|
||||||
|
:for="`${id || 'radio'}-${option.value}`"
|
||||||
|
class="flex items-center gap-2 text-primary-500"
|
||||||
|
:class="itemClass"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`${id || 'radio'}-${option.value}`"
|
||||||
|
type="radio"
|
||||||
|
:name="name || id || 'radio-group'"
|
||||||
|
:value="String(option.value)"
|
||||||
|
:checked="String(modelValue ?? '') === String(option.value)"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="h-4 w-4 border-slate-300 text-primary-500 focus:ring-primary-500"
|
||||||
|
:class="[
|
||||||
|
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
inputClass
|
||||||
|
]"
|
||||||
|
@change="onChange"
|
||||||
|
>
|
||||||
|
<span class="text-xl" :class="optionLabelClass">
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAttrs } from 'vue'
|
||||||
|
|
||||||
|
type RadioOption = {
|
||||||
|
value: string | number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
label?: string
|
||||||
|
modelValue: string | number | null | undefined
|
||||||
|
options: RadioOption[]
|
||||||
|
disabled?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
groupClass?: string
|
||||||
|
itemClass?: string
|
||||||
|
inputClass?: string
|
||||||
|
optionLabelClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
label: '',
|
||||||
|
disabled: false,
|
||||||
|
wrapperClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
groupClass: '',
|
||||||
|
itemClass: '',
|
||||||
|
inputClass: '',
|
||||||
|
optionLabelClass: ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const onChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="font-bold uppercase text-xl mb-2"
|
class="font-bold uppercase text-xl text-primary-500"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
:value="modelValue ?? ''"
|
:value="modelValue ?? ''"
|
||||||
:disabled="disabled || loading"
|
:disabled="disabled || loading"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
|
class="border-b border-black justify-self-start text-xl text-primary-500 py-[6px] bg-transparent"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
|
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="font-bold uppercase text-xl mb-2"
|
class="font-bold uppercase text-xl text-primary-500"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
:maxlength="maxlength"
|
:maxlength="maxlength"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
class="border-b border-black text-xl pb-[6px] bg-transparent"
|
class="border-b border-black text-xl py-[6px] bg-transparent text-primary-500"
|
||||||
:class="[
|
:class="[
|
||||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<label :for="inputId" class="font-bold uppercase text-xl mb-2">{{ label }}</label>
|
<label :for="inputId" class="font-bold uppercase text-xl text-primary-500">{{ label }}</label>
|
||||||
<div class="flex items-end gap-8">
|
<div class="flex items-end gap-8">
|
||||||
<input
|
<input
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:maxlength="maxLength"
|
:maxlength="maxLength"
|
||||||
:placeholder="placeholderText"
|
:placeholder="placeholderText"
|
||||||
class="border-b border-black flex-1 min-w-0 text-xl uppercase h-[30px]"
|
class="border-b border-black flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
<UiCheckbox
|
<UiCheckbox
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(label, index) in labels"
|
v-for="(label, index) in labels"
|
||||||
:key="label"
|
:key="label"
|
||||||
class="absolute top-0 whitespace-nowrap"
|
class="absolute top-0 whitespace-nowrap text-primary-500"
|
||||||
:class="labelClass(index)"
|
:class="labelClass(index)"
|
||||||
:style="positionStyle(index)"
|
:style="positionStyle(index)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
export enum StepLabel {
|
export enum StepLabel {
|
||||||
Reception = 'Réception',
|
Reception = 'Réception',
|
||||||
GrossWeighing = 'Pesée à plein',
|
GrossWeighing = 'Pesée à plein',
|
||||||
Selection = 'Sélection réceptionnées',
|
Selection = 'Sélection réception',
|
||||||
TareWeighing = 'Pesée à vide',
|
TareWeighing = 'Pesée à vide',
|
||||||
Shipment = 'Expédition',
|
Shipment = 'Expédition',
|
||||||
|
ShipmentLoading = 'Chargement',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RECEPTION_STEP_LABELS = [
|
export const RECEPTION_STEP_LABELS = [
|
||||||
@@ -16,5 +17,6 @@ export const RECEPTION_STEP_LABELS = [
|
|||||||
export const SHIPMENT_STEP_LABELS = [
|
export const SHIPMENT_STEP_LABELS = [
|
||||||
StepLabel.Shipment,
|
StepLabel.Shipment,
|
||||||
StepLabel.TareWeighing,
|
StepLabel.TareWeighing,
|
||||||
|
StepLabel.ShipmentLoading,
|
||||||
StepLabel.GrossWeighing,
|
StepLabel.GrossWeighing,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"create": "Fournisseur créé avec succès.",
|
"create": "Fournisseur créé avec succès.",
|
||||||
"update": "Fournisseur mis à jour avec succès."
|
"update": "Fournisseur mis à jour avec succès."
|
||||||
},
|
},
|
||||||
|
"customer": {
|
||||||
|
"create": "Client créé avec succès.",
|
||||||
|
"update": "Client mis à jour avec succès."
|
||||||
|
},
|
||||||
"address": {
|
"address": {
|
||||||
"create": "Adresse créée avec succès.",
|
"create": "Adresse créée avec succès.",
|
||||||
"update": "Adresse mise à jour avec succès."
|
"update": "Adresse mise à jour avec succès."
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-screen text-neutral-900 grid grid-rows-[85px,1fr]">
|
|
||||||
<!-- HEADER -->
|
|
||||||
<header class="bg-primary-500 z-50 h-[85px]">
|
|
||||||
<div class="h-full w-full px-6 grid grid-cols-[auto,1fr,auto] items-center gap-8">
|
|
||||||
<NuxtLink to="/" class="grid place-items-center">
|
|
||||||
<span class="grid place-items-center bg-white text-xl font-bold uppercase text-primary-500 p-4">
|
|
||||||
LOGO
|
|
||||||
</span>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<nav class="text-2xl font-bold uppercase text-white"></nav>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="text-xl font-bold uppercase text-white transition hover:opacity-80 justify-self-end"
|
|
||||||
>
|
|
||||||
Quitter le panel admin
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-[16rem,1fr] h-[calc(100vh-85px)] min-h-0">
|
|
||||||
<aside class="bg-primary-500 text-white min-h-0 flex flex-col justify-between">
|
|
||||||
<div class="flex flex-col gap-4 p-4 font-bold text-xl">
|
|
||||||
<!-- Liste des liens à ajouter ci-dessous -->
|
|
||||||
<NuxtLink to="/admin/dashboard">
|
|
||||||
Tableau de bord
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/admin/supplier/supplier-list">
|
|
||||||
Fournisseur
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/admin/carrier/carrier-list">
|
|
||||||
Transporteur
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/admin/user/list">
|
|
||||||
Utilisateurs
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4">
|
|
||||||
<p class="font-bold text-white text-left">v{{ version }}</p>
|
|
||||||
<button
|
|
||||||
@click="handleLogout"
|
|
||||||
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
|
|
||||||
>
|
|
||||||
Déconnexion
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main class="min-h-0 overflow-auto px-12 py-12 ">
|
|
||||||
<div class="w-full ">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {useAuthStore} from '~/stores/auth'
|
|
||||||
|
|
||||||
const auth = useAuthStore()
|
|
||||||
const { version } = useAppVersion()
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await auth.logout()
|
|
||||||
} finally {
|
|
||||||
await navigateTo('/login')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,53 +1,147 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-white text-neutral-900">
|
<div class="min-h-screen text-neutral-900 flex flex-col">
|
||||||
<header class="w-full border-b border-neutral-200 bg-primary-500">
|
<!-- HEADER -->
|
||||||
<div class="flex w-full items-center justify-center px-6 py-4">
|
<header class="w-full bg-primary-500 py-5 px-6">
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<!-- Burger (mobile) -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center justify-center text-3xl text-white md:hidden"
|
class="inline-flex items-center justify-center text-3xl text-white md:hidden"
|
||||||
aria-label="Ouvrir le menu"
|
aria-label="Ouvrir le menu"
|
||||||
@click="toggleMenu"
|
@click="toggleMenu"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" class="flex items-center"><Icon name="mdi:menu" size="44"/></span>
|
<span aria-hidden="true" class="flex items-center">
|
||||||
|
<Icon name="mdi:menu" size="44"/>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<nav class="ml-4 hidden items-center gap-8 text-2xl font-bold uppercase text-white md:flex">
|
|
||||||
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }">
|
<!-- Logo -->
|
||||||
|
<NuxtLink to="/" class="shrink-0">
|
||||||
|
<span class="flex items-center justify-center bg-white text-xl font-bold uppercase px-6 py-4">
|
||||||
|
LOGO
|
||||||
|
</span>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<!-- NAV centré (desktop) -->
|
||||||
|
<nav
|
||||||
|
class="hidden md:flex flex-1 items-center justify-center gap-8 text-xl font-bold uppercase text-white"
|
||||||
|
>
|
||||||
|
<NuxtLink to="/" custom v-slot="{ href, navigate }">
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
:class="isExactActive ? 'opacity-100' : 'opacity-50'"
|
:class="route.path === '/'
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
>
|
>
|
||||||
Accueil
|
Accueil
|
||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/dashboard" custom v-slot="{ href, navigate, isActive }"
|
|
||||||
v-if="auth.isAdmin"
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/supplier/supplier-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/supplier')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
>
|
>
|
||||||
Admin
|
Fournisseurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/carrier/carrier-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/carrier')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Transporteurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/user/list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/user')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Utilisateurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
to="/admin/customer/customer-list"
|
||||||
|
custom
|
||||||
|
v-slot="{ href, navigate }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/customer')
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-65 hover:opacity-100 transition'"
|
||||||
|
>
|
||||||
|
Clients
|
||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
<NuxtLink to="/" class="flex flex-1 items-center justify-center gap-3">
|
|
||||||
<span
|
<!-- Spacer mobile (pour centrer visuellement le header si besoin) -->
|
||||||
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
|
|
||||||
>
|
|
||||||
LOGO
|
|
||||||
</span>
|
|
||||||
</NuxtLink>
|
|
||||||
<div class="w-[44px] md:hidden"></div>
|
<div class="w-[44px] md:hidden"></div>
|
||||||
<button
|
|
||||||
type="button"
|
<!-- User dropdown à droite (desktop) -->
|
||||||
class="ml-auto hidden text-xl font-bold uppercase text-white transition hover:opacity-80 md:inline-flex"
|
<div v-if="auth.isAuthenticated" class="ml-auto relative hidden md:flex items-center text-white group">
|
||||||
@click="handleLogout"
|
<button
|
||||||
>
|
type="button"
|
||||||
Déconnexion
|
class="inline-flex items-center py-2 -my-2 text-xl leading-none transition hover:opacity-80"
|
||||||
</button>
|
aria-haspopup="true"
|
||||||
|
>
|
||||||
|
<span class="capitalize font-bold">{{ userDisplayName }}</span>
|
||||||
|
<span class="ml-[6px] inline-flex items-center font-bold transition-transform group-hover:rotate-180 group-focus-within:rotate-180">
|
||||||
|
<Icon name="mdi:chevron-down" size="20"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute right-0 top-full z-10 w-56 rounded-md bg-primary-500 py-2 border-neutral-300 border shadow-lg
|
||||||
|
opacity-0 invisible pointer-events-none transition
|
||||||
|
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto
|
||||||
|
group-focus-within:opacity-100 group-focus-within:visible group-focus-within:pointer-events-auto"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-4 py-2 text-left text-sm font-semibold text-white opacity-85 hover:opacity-100 transition"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay (mobile) -->
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition duration-200 ease-out"
|
enter-active-class="transition duration-200 ease-out"
|
||||||
enter-from-class="opacity-0"
|
enter-from-class="opacity-0"
|
||||||
@@ -62,6 +156,8 @@
|
|||||||
@click="closeMenu"
|
@click="closeMenu"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
|
<!-- Drawer (mobile) -->
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition duration-200 ease-out"
|
enter-active-class="transition duration-200 ease-out"
|
||||||
enter-from-class="-translate-x-full"
|
enter-from-class="-translate-x-full"
|
||||||
@@ -72,9 +168,7 @@
|
|||||||
>
|
>
|
||||||
<aside
|
<aside
|
||||||
v-if="isMenuOpen"
|
v-if="isMenuOpen"
|
||||||
class="fixed left-0 top-0 z-50 h-full w-full bg-primary-600 px-6 pb-8 pt-6 text-white shadow-xl md:hidden"
|
class="fixed left-0 top-0 z-50 h-full w-full bg-primary-500 px-6 pb-8 pt-6 text-white shadow-xl md:hidden"
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-2xl font-bold uppercase">Menu</span>
|
<span class="text-2xl font-bold uppercase">Menu</span>
|
||||||
@@ -87,12 +181,27 @@
|
|||||||
<Icon name="mdi:close" size="44"/>
|
<Icon name="mdi:close" size="44"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase">
|
<nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase">
|
||||||
<NuxtLink to="/" class="opacity-100" @click="closeMenu">Accueil</NuxtLink>
|
<NuxtLink to="/admin/dashboard" @click="closeMenu">Accueil</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/supplier/supplier-list" @click="closeMenu">
|
||||||
|
Fournisseurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/carrier/carrier-list" @click="closeMenu">
|
||||||
|
Transporteurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/user/list" @click="closeMenu">
|
||||||
|
Utilisateurs
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink v-if="auth.isAdmin" to="/admin/customer/customer-list" @click="closeMenu">
|
||||||
|
Clients
|
||||||
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="auth.isAuthenticated"
|
||||||
type="button"
|
type="button"
|
||||||
class="mt-5 text-xl font-bold uppercase"
|
class="mt-6 text-xl font-bold uppercase"
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
>
|
>
|
||||||
Déconnexion
|
Déconnexion
|
||||||
@@ -100,10 +209,10 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</transition>
|
</transition>
|
||||||
</header>
|
</header>
|
||||||
<main class="mx-auto w-full max-w-[1280px] pb-0">
|
<main class="mx-auto w-full max-w-[1280px] mt-16">
|
||||||
<slot/>
|
<slot/>
|
||||||
</main>
|
</main>
|
||||||
<footer class="w-full mt-8 bg-primary-500 p-6">
|
<footer class="w-full mt-auto bg-primary-500 px-6 py-3">
|
||||||
<p class="font-bold text-white text-right">v{{ version }}</p>
|
<p class="font-bold text-white text-right">v{{ version }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,9 +223,12 @@ import {useAuthStore} from '~/stores/auth'
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const isMenuOpen = ref(false)
|
|
||||||
const {version} = useAppVersion()
|
const {version} = useAppVersion()
|
||||||
|
|
||||||
|
const isMenuOpen = ref(false)
|
||||||
|
|
||||||
|
const userDisplayName = computed(() => auth.user?.username ?? 'Utilisateur')
|
||||||
|
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
isMenuOpen.value = false
|
isMenuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="flex items-center justify-between ">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-3xl font-bold uppercase">
|
<h1 class="text-3xl font-bold uppercase">
|
||||||
{{ route.params.id ? 'Modifier transporteur' : 'Ajout transporteur' }}
|
{{ route.params.id ? 'Modifier transporteur' : 'Ajout transporteur' }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
>Enregistrer
|
>Enregistrer
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 py-12">
|
||||||
<UiTextInput
|
<UiTextInput
|
||||||
label = "nom du fournisseur"
|
label = "nom du fournisseur"
|
||||||
id="carrier-name"
|
id="carrier-name"
|
||||||
@@ -33,19 +33,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier";
|
import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier";
|
||||||
import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data";
|
import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data";
|
||||||
|
import {computed} from "vue";
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const idCarrier = Number(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 resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
const form = reactive<CarrierFormData>({
|
const form = reactive<CarrierFormData>({
|
||||||
code:'',
|
code:'',
|
||||||
name:''
|
name:''
|
||||||
})
|
})
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'default'
|
||||||
})
|
})
|
||||||
|
|
||||||
const hydrateFromUser = (carrier: CarrierData | null) => {
|
const hydrateFromUser = (carrier: CarrierData | null) => {
|
||||||
@@ -59,7 +67,7 @@ const hydrateFromUser = (carrier: CarrierData | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => idCarrier,
|
() => idCarrier.value,
|
||||||
async (id) => {
|
async (id) => {
|
||||||
if (id === null) {
|
if (id === null) {
|
||||||
return
|
return
|
||||||
@@ -85,8 +93,8 @@ async function validate() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(idCarrier){
|
if(idCarrier.value){
|
||||||
await updateCarrier(idCarrier, basePayload)
|
await updateCarrier(idCarrier.value, basePayload)
|
||||||
navigate()
|
navigate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="flex items-center justify-between ">
|
<div class="flex items-center justify-between ">
|
||||||
<h1 class="text-3xl font-bold uppercase">listes des transporteurs</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des transporteurs</h1>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/carrier"
|
to="/admin/carrier"
|
||||||
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded"
|
||||||
>Ajouter
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ const goToCarrier = (id: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'default'
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
197
frontend/pages/admin/customer/[[id]].vue
Normal file
197
frontend/pages/admin/customer/[[id]].vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-3xl font-bold uppercase">
|
||||||
|
{{ customerId ? "Modifications du client" : "Ajout d'un client" }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
{{ customerId ? "Sauvegarder" : "Ajouter" }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12">
|
||||||
|
<UiTextInput id="customer-name" v-model="form.name" label="Nom du client" :disabled="!auth.isAdmin"/>
|
||||||
|
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin"/>
|
||||||
|
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-24 mb-4 py-6 border-t border-black"></div>
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-3xl font-bold uppercase">Adresses client</h2>
|
||||||
|
<UiButton
|
||||||
|
type="button"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="customerId === null || !auth.isAdmin"
|
||||||
|
@click="goToAddAddress"
|
||||||
|
>
|
||||||
|
Ajouter
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto mb-10">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-gray-200">
|
||||||
|
<th class="py-3 pr-4 text-sm uppercase">Libellé</th>
|
||||||
|
<th class="py-3 pr-4 text-sm uppercase">Rue</th>
|
||||||
|
<th class="py-3 pr-4 text-sm uppercase">Complément</th>
|
||||||
|
<th class="py-3 pr-4 text-sm uppercase">Code postal</th>
|
||||||
|
<th class="py-3 pr-4 text-sm uppercase">Ville</th>
|
||||||
|
<th class="py-3 pr-4 text-sm uppercase">Pays</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-if="form.addresses.length === 0">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="py-4 text-slate-400">
|
||||||
|
Aucune adresse.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr
|
||||||
|
v-for="(address, index) in form.addresses"
|
||||||
|
:key="address.id ?? index"
|
||||||
|
class="border-b border-gray-100 hover:bg-slate-50"
|
||||||
|
:class="auth.isAdmin ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="goToEditAddress(address.id ?? null)"
|
||||||
|
>
|
||||||
|
<td class="py-3 pr-4">{{ address.label || "—" }}</td>
|
||||||
|
<td class="py-3 pr-4">{{ address.street || "—" }}</td>
|
||||||
|
<td class="py-3 pr-4">{{ address.street2 || "—" }}</td>
|
||||||
|
<td class="py-3 pr-4">{{ address.postalCode || "—" }}</td>
|
||||||
|
<td class="py-3 pr-4">{{ address.city || "—" }}</td>
|
||||||
|
<td class="py-3 pr-4">{{ address.countryCode || "—" }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, reactive, ref, watch} from "vue"
|
||||||
|
import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
|
||||||
|
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
|
||||||
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
|
definePageMeta({layout: "default"})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
const customerId = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const form = reactive<CustomerFormData>({
|
||||||
|
name: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
addresses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToAddAddress = () => {
|
||||||
|
if (customerId.value === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToEditAddress = (addressId: number | null) => {
|
||||||
|
if (customerId.value === null || addressId === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/customer/address",
|
||||||
|
query: {
|
||||||
|
customerId: String(customerId.value),
|
||||||
|
addressId: String(addressId),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFromCustomer = (customer: CustomerData | null) => {
|
||||||
|
if (!customer) return
|
||||||
|
form.name = customer.name ?? ""
|
||||||
|
form.phone = customer.phone ?? ""
|
||||||
|
form.email = customer.email ?? ""
|
||||||
|
if (!Array.isArray(customer.addresses) || customer.addresses.length === 0) {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof customer.addresses[0] === "string") {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addresses = customer.addresses.map((address) => ({
|
||||||
|
id: address.id ?? null,
|
||||||
|
label: address.label ?? "",
|
||||||
|
street: address.street ?? "",
|
||||||
|
street2: address.street2 ?? null,
|
||||||
|
postalCode: address.postalCode ?? "",
|
||||||
|
city: address.city ?? "",
|
||||||
|
countryCode: address.countryCode ?? "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => customerId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const customer = await getCustomer(id)
|
||||||
|
hydrateFromCustomer(customer)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = form.name.trim()
|
||||||
|
const phone = form.phone?.trim() || null
|
||||||
|
const email = form.email?.trim() || null
|
||||||
|
|
||||||
|
const customerPayload: CustomerPayload = {
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
|
if (customerId.value !== null) {
|
||||||
|
await updateCustomer(customerId.value, customerPayload)
|
||||||
|
targetId = customerId.value
|
||||||
|
} else {
|
||||||
|
const created = await createCustomer(customerPayload)
|
||||||
|
targetId = created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId !== null) {
|
||||||
|
await router.push(`/admin/customer/${targetId}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
49
frontend/pages/admin/customer/address.vue
Normal file
49
frontend/pages/admin/customer/address.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<Address type="customer" :address="address" @validate="validate"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AddressData, AddressPayload } from "~/services/address"
|
||||||
|
import { createAddress, getAddress, updateAddress } from "~/services/address"
|
||||||
|
import { getCustomer, updateCustomer } from "~/services/customer"
|
||||||
|
import type { CustomerData } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
|
definePageMeta({ layout: "default" })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const customerId = computed(() => Number(route.query.customerId))
|
||||||
|
const customer = ref<CustomerData | null>(null)
|
||||||
|
const addressId = computed(() => (route.query.addressId !== undefined ? Number(route.query.addressId) : null))
|
||||||
|
const address = ref<AddressData | null>(null)
|
||||||
|
|
||||||
|
const validate = async (payload: AddressPayload) => {
|
||||||
|
try {
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
await updateAddress(addressId.value, payload)
|
||||||
|
} else {
|
||||||
|
await addAddress(payload)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await router.push("/admin/customer/" + customerId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = async (payload: AddressPayload) => {
|
||||||
|
const response: AddressData = await createAddress(payload)
|
||||||
|
const addressIRI = `/api/addresses/${response.id}`
|
||||||
|
const existingIris = (customer.value?.addresses ?? [])
|
||||||
|
.map((item: any) => (typeof item === "string" ? item : `/api/addresses/${item.id}`))
|
||||||
|
.filter((iri: string | null) => Boolean(iri)) as string[]
|
||||||
|
const next = [...new Set([...existingIris, addressIRI])]
|
||||||
|
|
||||||
|
return await updateCustomer(customerId.value, { addresses: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
customer.value = await getCustomer(customerId.value)
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
address.value = await getAddress(addressId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
117
frontend/pages/admin/customer/customer-list.vue
Normal file
117
frontend/pages/admin/customer/customer-list.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-3xl font-bold uppercase text-primary-500">Liste des Clients</h1>
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/customer"
|
||||||
|
class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded-md"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
|
Ajouter
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-6 border border-slate-200 mb-16">
|
||||||
|
<div class="max-h-96 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="sticky top-0 z-10 grid grid-cols-8 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<div>Nom</div>
|
||||||
|
<div>Téléphone</div>
|
||||||
|
<div>Email</div>
|
||||||
|
<div>Rue</div>
|
||||||
|
<div>Complément</div>
|
||||||
|
<div>Code Postal</div>
|
||||||
|
<div>Ville</div>
|
||||||
|
<div>Pays</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="customerList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun client.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="customer in customerList" :key="customer.id">
|
||||||
|
<div
|
||||||
|
v-if="!customer.addresses || customer.addresses.length === 0"
|
||||||
|
class="grid grid-cols-8 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
||||||
|
@click="goToCustomer(customer.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ customer.name || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.phone || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.email || "—" }}</div>
|
||||||
|
<div class="col-span-1">Pas d'adresse</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="customer.addresses.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(address, idx) in customer.addresses"
|
||||||
|
:key="address.id ?? `${customer.id}-${idx}-${address.street}-${address.postalCode}`"
|
||||||
|
class="grid grid-cols-8 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 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
@click="goToCustomer(customer.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ customer.name || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.phone || "—" }}</div>
|
||||||
|
<div class="truncate">{{ customer.email || "—" }}</div>
|
||||||
|
<div class="col-span-5 text-slate-400">
|
||||||
|
Adresses non chargées
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-6 border border-slate-200 mb-16 px-4 py-6 text-slate-400">
|
||||||
|
Accès réservé aux administrateurs.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getCustomerList } from "~/services/customer"
|
||||||
|
import type { CustomerData } from "~/services/dto/customer-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
|
definePageMeta({ layout: "default" })
|
||||||
|
|
||||||
|
const customerList = ref<CustomerData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToCustomer = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/customer/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
customerList.value = await getCustomerList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'default'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="flex items-center justify-between gap-10">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-3xl font-bold uppercase">
|
<h1 class="text-3xl font-bold uppercase">
|
||||||
{{ supplierId ? "Modifications du fournisseur" : "Ajout d'un fournisseur" }}
|
{{ supplierId ? "Modifications du fournisseur" : "Ajout d'un fournisseur" }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<button
|
<UiButton
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isLoading || !auth.isAdmin"
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
>
|
>
|
||||||
{{ supplierId ? "Sauvegarder" : "Ajouter" }}
|
{{ supplierId ? "Sauvegarder" : "Ajouter" }}
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-y-16 gap-x-12 mb-10 py-12 border-b border-black ">
|
<div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12">
|
||||||
<UiTextInput id="supplier-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin"/>
|
<UiTextInput id="supplier-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin"/>
|
||||||
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin"/>
|
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin"/>
|
||||||
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin"/>
|
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-4 py-6 border-t border-black"></div>
|
<div class="mx-24 mb-4 py-6 border-t border-black"></div>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-3xl font-bold uppercase">Adresses fournisseur</h2>
|
<h2 class="text-3xl font-bold uppercase">Adresses fournisseur</h2>
|
||||||
<button
|
<UiButton
|
||||||
type="button"
|
type="button"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
:disabled="supplierId === null || !auth.isAdmin"
|
:disabled="supplierId === null || !auth.isAdmin"
|
||||||
@click="goToAddAddress"
|
@click="goToAddAddress"
|
||||||
>
|
>
|
||||||
Ajouter
|
Ajouter
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto mb-10">
|
<div class="overflow-x-auto mb-10">
|
||||||
<table class="w-full border-collapse">
|
<table class="w-full border-collapse">
|
||||||
@@ -80,7 +80,7 @@ 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 {useAuthStore} from "~/stores/auth"
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
definePageMeta({layout: "admin"})
|
definePageMeta({layout: "default"})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -179,14 +179,17 @@ async function validate() {
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
}
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
if (supplierId.value !== null) {
|
if (supplierId.value !== null) {
|
||||||
await updateSupplier(supplierId.value, supplierPayload)
|
await updateSupplier(supplierId.value, supplierPayload)
|
||||||
|
targetId = supplierId.value
|
||||||
} else {
|
} else {
|
||||||
await createSupplier(supplierPayload)
|
const created = await createSupplier(supplierPayload)
|
||||||
|
targetId = created.id
|
||||||
}
|
}
|
||||||
|
|
||||||
await router.push("/admin/supplier/supplier-list")
|
await router.push(`/admin/supplier/${targetId}`)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {createAddress, getAddress, updateAddress} from "~/services/address";
|
|||||||
import {getSupplier, updateSupplier} from "~/services/supplier";
|
import {getSupplier, updateSupplier} from "~/services/supplier";
|
||||||
import type {SupplierData} from "~/services/dto/supplier-data";
|
import type {SupplierData} from "~/services/dto/supplier-data";
|
||||||
|
|
||||||
definePageMeta({ layout: "admin" })
|
definePageMeta({ layout: "default" })
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-3xl font-bold uppercase">Fournisseurs</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">Liste des fournisseurs</h1>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/supplier"
|
to="/admin/supplier"
|
||||||
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded"
|
||||||
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
@click="handleAddClick"
|
@click="handleAddClick"
|
||||||
>
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
Ajouter
|
Ajouter
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +90,7 @@ import { getSupplierList } from "~/services/supplier"
|
|||||||
import type { SupplierData } from "~/services/dto/supplier-data"
|
import type { SupplierData } from "~/services/dto/supplier-data"
|
||||||
import { useAuthStore } from "~/stores/auth"
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
definePageMeta({ layout: "admin" })
|
definePageMeta({ layout: "default" })
|
||||||
|
|
||||||
const supplierList = ref<SupplierData[]>([])
|
const supplierList = ref<SupplierData[]>([])
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
<h1 class="text-3xl font-bold uppercase">
|
<h1 class="text-3xl font-bold uppercase">
|
||||||
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
|
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<UiButton
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{{ userId ? 'Sauvegarder' : 'Ajouter' }}
|
{{ userId ? 'Sauvegarder' : 'Ajouter' }}
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-y-16 gap-x-40 mb-16">
|
<div class="grid gap-y-16 gap-x-40 py-12">
|
||||||
<UiTextInput
|
<UiTextInput
|
||||||
id="user-name"
|
id="user-name"
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
@@ -39,13 +39,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'default'
|
||||||
})
|
})
|
||||||
|
|
||||||
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} from '~/services/dto/user-data'
|
import type {UserData, UserFormData, UserPayload} from '~/services/dto/user-data'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -105,10 +105,12 @@ async function validate() {
|
|||||||
const normalizedRole = form.role.trim()
|
const normalizedRole = form.role.trim()
|
||||||
const normalizedPassword = form.password.trim()
|
const normalizedPassword = form.password.trim()
|
||||||
|
|
||||||
const basePayload = {
|
const basePayload: UserPayload = {
|
||||||
username: normalizedUsername,
|
username: normalizedUsername,
|
||||||
roles: normalizedRole ? [normalizedRole] : undefined,
|
roles: normalizedRole ? [normalizedRole] : undefined,
|
||||||
password: normalizedPassword || undefined
|
}
|
||||||
|
if (normalizedPassword) {
|
||||||
|
basePayload.password = normalizedPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId.value) {
|
if (userId.value) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-3xl font-bold uppercase">Liste des utilisateurs</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">Liste des utilisateurs</h1>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded-md"
|
||||||
@click="router.push('/admin/user/')"
|
@click="router.push('/admin/user/')"
|
||||||
>
|
>
|
||||||
|
<Icon name="mdi:plus" size="28" />
|
||||||
Ajouter
|
Ajouter
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ user.roles?.join(', ') || ' ---' }}
|
{{ getRoleLabels(user.roles) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,19 +39,31 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'default'
|
||||||
})
|
})
|
||||||
|
|
||||||
import type {UserData} from "~/services/dto/user-data";
|
import type {UserData} from "~/services/dto/user-data";
|
||||||
import {getAdminUsers, getUsers} from "~/services/auth";
|
import {getAdminUsers} from "~/services/auth";
|
||||||
|
import {ROLE} from "~/utils/constants";
|
||||||
|
|
||||||
const userList = ref<UserData[]>([])
|
const userList = ref<UserData[]>([])
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const roleLabelByValue = new Map(ROLE.map((role) => [role.value, role.label]))
|
||||||
|
|
||||||
const goToUser = (id: number) => {
|
const goToUser = (id: number) => {
|
||||||
router.push(`/admin/user/${id}`)
|
router.push(`/admin/user/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRoleLabels = (roles?: string[]) => {
|
||||||
|
if (!roles || roles.length === 0) {
|
||||||
|
return ' ---'
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles
|
||||||
|
.map((role) => roleLabelByValue.get(role) ?? role)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
userList.value = await getAdminUsers()
|
userList.value = await getAdminUsers()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap justify-center mt-8 gap-8 mb-8 md:mb-0">
|
<div class="flex flex-wrap justify-center pb-16 gap-12">
|
||||||
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
||||||
<card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" />
|
<card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" />
|
||||||
<card-link label="PLAN DE SITE" link="/" iconName="mdi:warehouse" />
|
<card-link label="PLAN DE SITE" link="/" iconName="material-symbols:warehouse-outline-rounded" />
|
||||||
<card-link label="RÉCEPTIONS EN ATTENTE" link="/reception/waiting-reception" iconName="mdi:truck-remove-outline" />
|
<card-link link="/reception/waiting-reception" iconName="mdi:truck-remove-outline">
|
||||||
<card-link label="EXPÉDITIONS EN ATTENTE" link="/shipment/waiting-shipment" iconName="mdi:truck-cargo-container" />
|
<template #label>
|
||||||
<card-link label="CASES" link="/" iconName="mdi:cube-outline" />
|
Réceptions<br>EN ATTENTE
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
|
<card-link link="/shipment/waiting-shipment" iconName="mdi:truck-cargo-container">
|
||||||
|
<template #label>
|
||||||
|
EXPÉDITIONS<br>EN ATTENTE
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
|
<card-link label="CASES" link="/" iconName="material-symbols:bottom-sheets-outline" />
|
||||||
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
|
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
|
||||||
<card-link label="EXPÉDITIONS FINIES" link="/shipment/finish-shipment" iconName="mdi:truck-delivery-outline" />
|
<card-link label="EXPÉDITIONS FINIES" link="/shipment/finish-shipment" iconName="mdi:truck-delivery-outline" />
|
||||||
<card-link label="PASSEPORT DU BOVIN" link="/" iconName="mdi:cow" />
|
<card-link link="/" iconName="mdi:cow">
|
||||||
|
<template #label>
|
||||||
|
PASSEPORT<br>DU BOVIN
|
||||||
|
</template>
|
||||||
|
</card-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -39,13 +39,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<UiButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</button>
|
</UiButton>
|
||||||
<p class="font-bold">v{{ version }}</p>
|
<p class="font-bold">v{{ version }}</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex justify-between h-[52px] mb-[80px]">
|
||||||
<div class="flex justify-between h-[52px] mb-[80px]">
|
<div class="flex flex-1 mr-16">
|
||||||
<div class="flex flex-1 mr-16">
|
<UiStepper
|
||||||
<UiStepper
|
:labels="RECEPTION_STEP_LABELS"
|
||||||
:labels="RECEPTION_STEP_LABELS"
|
:current-step="storeReception?.currentStep ?? 0"
|
||||||
:current-step="storeReception?.currentStep ?? 0"
|
@select="handleStepSelect"
|
||||||
@select="handleStepSelect"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
|
||||||
@click="saveAndHold"
|
|
||||||
>Mettre en attente</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
<UiButton
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
type="button"
|
||||||
<ReceptionProductReceived
|
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
||||||
v-if="storeReception?.currentStep === 2 &&
|
@click="saveAndHold"
|
||||||
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
|
>Mettre en attente
|
||||||
<ReceptionBovineReceived
|
</UiButton>
|
||||||
v-if="storeReception?.currentStep === 2 &&
|
|
||||||
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
|
||||||
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||||
|
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
||||||
|
<ReceptionProductReceived
|
||||||
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
|
||||||
|
<ReceptionBovineReceived
|
||||||
|
v-if="storeReception?.currentStep === 2 &&
|
||||||
|
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
||||||
|
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-start gap-10">
|
<div class="flex items-center justify-start gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" />
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase">listes des réceptions finie</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions finie</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ps-20 " >
|
<div class="px-[86px]">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
<div>Numéro</div>
|
<div>Numéro</div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<div>{{ reception.supplier?.name }}</div>
|
<div>{{ reception.supplier?.name }}</div>
|
||||||
<div>{{ reception.address?.fullAddress }}</div>
|
<div>{{ reception.address?.fullAddress }}</div>
|
||||||
<div>{{ reception.receptionType?.label }}</div>
|
<div>{{ reception.receptionType?.label }}</div>
|
||||||
<div>{{ formatWeighing(reception, 'gross') }} | {{ formatWeighing(reception, 'tare') }}</div>
|
<div>{{ formatWeighing(reception) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,16 +36,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {ReceptionData} from "~/services/dto/reception-data";
|
import type {ReceptionData} from "~/services/dto/reception-data";
|
||||||
import {getReceptionList} from "~/services/reception";
|
import {getReceptionList} from "~/services/reception";
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
|
||||||
const receptionList = ref<ReceptionData[]>()
|
const receptionList = ref<ReceptionData[]>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const formatWeighing = (reception: ReceptionData, type: 'gross' | 'tare') => {
|
const formatWeighing = (reception: ReceptionData) => {
|
||||||
const entry = reception.weights?.find((weight) => weight.type === type)
|
const gross = reception.weights?.find((weight) => weight.type === 'gross')?.weight
|
||||||
if (!entry || entry.weight == null || entry.dsd == null) {
|
const tare = reception.weights?.find((weight) => weight.type === 'tare')?.weight
|
||||||
|
|
||||||
|
if (gross == null || tare == null) {
|
||||||
return '—'
|
return '—'
|
||||||
}
|
}
|
||||||
return `${entry.weight} kg`
|
|
||||||
|
return `${gross - tare} kg`
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToReception = (id: number) => {
|
const goToReception = (id: number) => {
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<form @submit.prevent="validate">
|
<form @submit.prevent="validate">
|
||||||
<div class="flex items-center justify-between mt-8 mb-8 ">
|
<div class="flex items-center justify-between mt-12 mb-8 ">
|
||||||
<h1 class="font-bold text-5xl uppercase">Réception {{receptionLoad?.identificationNumber}}</h1>
|
<h1 class="font-bold text-5xl uppercase">Réception {{ receptionLoad?.identificationNumber }}</h1>
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
|
||||||
:disabled="!auth.isAdmin"
|
|
||||||
>Enregistrer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<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-12">
|
||||||
<!-- Nom de l'utilisateur -->
|
<!-- Nom de l'utilisateur -->
|
||||||
<UiSelect
|
<UiSelect
|
||||||
id="reception-user"
|
id="reception-user"
|
||||||
@@ -120,28 +114,50 @@
|
|||||||
wrapper-class="col-start-2 row-start-4"
|
wrapper-class="col-start-2 row-start-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
|
<div class="flex justify-center mb-2">
|
||||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1" @click="isBtWeight = true" >pesées</h1>
|
<UiButton
|
||||||
<h1 class="font-bold text-5xl uppercase col-start-2 row-start-1" @click="isBtWeight = false">{{isMerchandise ? "Marchandises" : "Bovins"}}</h1>
|
v-if="auth.isAdmin"
|
||||||
|
type="submit"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] mb-16"
|
||||||
|
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<update-weight
|
<div class="flex justify-evenly gap-y-8 gap-x-40 mb-8 border-b border-slate-400">
|
||||||
v-if="isBtWeight"
|
<h1
|
||||||
:idReception="idReception"
|
class="font-bold text-3xl uppercase col-start-1 row-start-1 cursor-pointer"
|
||||||
:disabled="!auth.isAdmin"
|
:class="activeTab === 'weights' ? 'underline' : ''"
|
||||||
/>
|
@click="activeTab = 'weights'"
|
||||||
|
>
|
||||||
|
pesées
|
||||||
|
</h1>
|
||||||
|
<h1
|
||||||
|
class="font-bold text-3xl uppercase col-start-2 row-start-1 cursor-pointer"
|
||||||
|
:class="activeTab === 'merchandise' ? 'underline' : ''"
|
||||||
|
@click="activeTab = 'merchandise'"
|
||||||
|
>
|
||||||
|
{{ isMerchandise ? "Marchandise" : "Bovins" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<update-merchandise
|
<update-weight
|
||||||
v-else-if="isMerchandise"
|
v-if="activeTab === 'weights'"
|
||||||
:idReception="idReception"
|
:idReception="idReception"
|
||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<update-bovin
|
<update-merchandise
|
||||||
v-else
|
v-else-if="activeTab === 'merchandise' && isMerchandise"
|
||||||
:idReception="idReception"
|
:idReception="idReception"
|
||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<update-bovin
|
||||||
|
v-else
|
||||||
|
:idReception="idReception"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -168,6 +184,7 @@ import UpdateWeight from "~/components/reception/update-weight.vue";
|
|||||||
import UpdateMerchandise from "~/components/reception/update-merchandise.vue";
|
import UpdateMerchandise from "~/components/reception/update-merchandise.vue";
|
||||||
import UpdateBovin from "~/components/reception/update-bovin.vue";
|
import UpdateBovin from "~/components/reception/update-bovin.vue";
|
||||||
|
|
||||||
|
const activeTab = ref<'weights' | 'merchandise'>('weights')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const receptionStore = useReceptionStore()
|
const receptionStore = useReceptionStore()
|
||||||
const form = reactive<ReceptionFormData>({
|
const form = reactive<ReceptionFormData>({
|
||||||
@@ -249,7 +266,7 @@ const clearReceptionBovines = async (receptionIri: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateFromUser = (reception: ReceptionData | null)=> {
|
const hydrateFromUser = (reception: ReceptionData | null) => {
|
||||||
if (!reception) {
|
if (!reception) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -378,7 +395,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
watch(
|
watch(
|
||||||
() => [form.supplierId, suppliers.value],
|
() => [form.supplierId, form.addressId, suppliers.value],
|
||||||
() => {
|
() => {
|
||||||
if (!form.supplierId) {
|
if (!form.supplierId) {
|
||||||
form.addressId = ''
|
form.addressId = ''
|
||||||
@@ -395,7 +412,11 @@ watch(
|
|||||||
(address) => String(address.id) === form.addressId
|
(address) => String(address.id) === form.addressId
|
||||||
)
|
)
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
form.addressId = ''
|
if (supplierAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(supplierAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{immediate: true}
|
||||||
@@ -532,7 +553,7 @@ async function validate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (idReception) {
|
if (idReception) {
|
||||||
const updated = await receptionStore.updateReception(idReception,{
|
const updated = await receptionStore.updateReception(idReception, {
|
||||||
...payload
|
...payload
|
||||||
})
|
})
|
||||||
if (updated) {
|
if (updated) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between ">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-10">
|
<div class="flex items-center gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" />
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase">listes des réceptions en attente</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions en attente</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ps-20 " >
|
<div class="px-[86px]">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
<div class="mt-6 border border-slate-200 mb-16">
|
||||||
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
<div>Fournisseur</div>
|
<div>Fournisseur</div>
|
||||||
<div>Adresse</div>
|
<div>Adresse</div>
|
||||||
|
|||||||
@@ -9,16 +9,17 @@
|
|||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<UiButton
|
||||||
type="button"
|
type="button"
|
||||||
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
|
||||||
@click="saveAndHold"
|
@click="saveAndHold"
|
||||||
>Mettre en attente
|
>Mettre en attente
|
||||||
</button>
|
</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"/>
|
<ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
|
||||||
<ShipmentWeight v-if="storeShipment?.currentStep >= 2" mode="tare"/>
|
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
||||||
|
<ShipmentWeight v-if="storeShipment?.currentStep === 3" mode="tare"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-start gap-10">
|
<div class="flex items-center justify-start gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/>
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase">listes des expéditions finie</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions finie</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ps-20 ">
|
<div class="px-[86px]">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
<div>Numéro</div>
|
<div>Numéro</div>
|
||||||
@@ -21,11 +21,11 @@
|
|||||||
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="goToshipment(shipment.id)"
|
@click="goShipment(shipment.id)"
|
||||||
>
|
>
|
||||||
<div>{{ shipment.identificationNumber }}</div>
|
<div>{{ shipment.identificationNumber }}</div>
|
||||||
<div>{{ shipment.shipmentDate }}</div>
|
<div>{{ shipment.shipmentDate }}</div>
|
||||||
<div>{{ shipment.customer?.label }}</div>
|
<div>{{ shipment.customer?.name }}</div>
|
||||||
<div>{{ shipment.address?.fullAddress }}</div>
|
<div>{{ shipment.address?.fullAddress }}</div>
|
||||||
<div>
|
<div>
|
||||||
<template v-if="formatBovinShipmentLines(shipment).length">
|
<template v-if="formatBovinShipmentLines(shipment).length">
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ formatWeighing(shipment, 'gross') }} | {{ formatWeighing(shipment, 'tare') }}</div>
|
<div>{{ formatWeighing(shipment) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,12 +51,15 @@ import {getShipmentList} from "~/services/shipment";
|
|||||||
const shipmentList = ref<ShipmentData[]>()
|
const shipmentList = ref<ShipmentData[]>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const formatWeighing = (shipment: ShipmentData, type: 'gross' | 'tare') => {
|
const formatWeighing = (shipment: ShipmentData) => {
|
||||||
const entry = shipment.weights?.find((weight) => weight.type === type)
|
const gross = shipment.weights?.find((weight) => weight.type === 'gross')?.weight
|
||||||
if (!entry || entry.weight == null || entry.dsd == null) {
|
const tare = shipment.weights?.find((weight) => weight.type === 'tare')?.weight
|
||||||
|
|
||||||
|
if (gross == null || tare == null) {
|
||||||
return '—'
|
return '—'
|
||||||
}
|
}
|
||||||
return `${entry.weight} kg`
|
|
||||||
|
return `${gross - tare} kg`
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatBovinShipmentLines = (shipment: ShipmentData) => {
|
const formatBovinShipmentLines = (shipment: ShipmentData) => {
|
||||||
@@ -71,8 +74,8 @@ const formatBovinShipmentLines = (shipment: ShipmentData) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToshipment = (id: number) => {
|
const goShipment = (id: number) => {
|
||||||
//router.push(`/shipment/update/${id}`)
|
router.push(`/shipment/update/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between ">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-10">
|
<div class="flex items-center gap-10">
|
||||||
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/>
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
|
||||||
<h1 class="text-3xl font-bold uppercase">listes des expéditions en attente</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions en attente</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ps-20 ">
|
<div class="px-[86px]">
|
||||||
<div class="mt-6 border border-slate-200 mb-16 ">
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
<div>Client</div>
|
<div>Client</div>
|
||||||
|
|||||||
@@ -1,23 +1,43 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from "~/composables/useApi"
|
||||||
import type { CustomerData } from '~/services/dto/customer-data'
|
import type { CustomerData, CustomerPayload } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
export type CustomerListResponse =
|
export type CustomerListResponse =
|
||||||
| CustomerData[]
|
| CustomerData[]
|
||||||
| { 'hydra:member'?: CustomerData[] }
|
| { "hydra:member"?: CustomerData[] }
|
||||||
|
|
||||||
export async function getCustomerList(): Promise<CustomerData[]> {
|
export async function getCustomerList(): Promise<CustomerData[]> {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const response = await api.get<CustomerListResponse>('customers', {}, {
|
const response = await api.get<CustomerListResponse>("customers", {}, {
|
||||||
toastErrorKey: 'errors.customer.list'
|
toastErrorKey: "errors.customer.list",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(response)) {
|
if (Array.isArray(response)) return response
|
||||||
return response
|
if (response && typeof response === "object" && Array.isArray(response["hydra:member"])) {
|
||||||
|
return response["hydra:member"]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
|
||||||
return response['hydra:member']
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCustomer(id: number): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<CustomerData>(`customers/${id}`, {}, {
|
||||||
|
toastErrorKey: "errors.customer.fetch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCustomer(id: number, payload: Partial<CustomerPayload>): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<CustomerData>(`customers/${id}`, payload, {
|
||||||
|
toastErrorKey: "errors.customer.update",
|
||||||
|
toastSuccessKey: "success.customer.update",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCustomer(payload: CustomerPayload): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<CustomerData>("customers", payload, {
|
||||||
|
toastErrorKey: "errors.customer.create",
|
||||||
|
toastSuccessKey: "success.customer.create",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface AddressData {
|
|||||||
postalCode: string
|
postalCode: string
|
||||||
city: string
|
city: string
|
||||||
countryCode: string
|
countryCode: string
|
||||||
|
fullAddress: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddressFormData {
|
export interface AddressFormData {
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
import type { AddressData } from "~/services/dto/address-data"
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
|
||||||
|
export type CustomerAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
export interface CustomerData {
|
export interface CustomerData {
|
||||||
id: number
|
id: number
|
||||||
label: string
|
name: string
|
||||||
code?: string | null
|
phone?: string | null
|
||||||
addresses?: AddressData[] | null
|
email?: string | null
|
||||||
|
addresses: CustomerAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerFormData {
|
||||||
|
name: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
addresses: AddressFormData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerPayload = {
|
||||||
|
name: string
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
addresses?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export interface WeightData {
|
|||||||
weight: number | null
|
weight: number | null
|
||||||
dsd: number | null
|
dsd: number | null
|
||||||
weighedAt: string | null
|
weighedAt: string | null
|
||||||
|
type : string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,7 @@ export default <Partial<Config>>{
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
50: '#f6f9ea',
|
500: '#456452',
|
||||||
100: '#eaf2cf',
|
|
||||||
200: '#d6e3a4',
|
|
||||||
300: '#c1d47a',
|
|
||||||
400: '#afc85a',
|
|
||||||
500: '#9ebb43',
|
|
||||||
600: '#7e9735',
|
|
||||||
700: '#607228',
|
|
||||||
800: '#414d1a',
|
|
||||||
900: '#24290d'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
migrations/Version20260213093000.php
Normal file
32
migrations/Version20260213093000.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213093000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add name, phone and email fields to customer.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ADD name VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD phone VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD email VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE customer SET name = label WHERE name IS NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN name SET NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer DROP name');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP phone');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP email');
|
||||||
|
}
|
||||||
|
}
|
||||||
37
migrations/Version20260213101500.php
Normal file
37
migrations/Version20260213101500.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213101500 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Align customer with supplier: keep name/email/phone and drop label/code.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN name TYPE VARCHAR(180)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(180)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(40)');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP COLUMN label');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP COLUMN code');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ADD label VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD code VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE customer SET label = name WHERE label IS NULL');
|
||||||
|
$this->addSql("UPDATE customer SET code = regexp_replace(upper(name), '[^A-Z0-9]+', '_', 'g') WHERE code IS NULL");
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN label SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN code SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(255)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(255)');
|
||||||
|
}
|
||||||
|
}
|
||||||
40
migrations/Version20260213114000.php
Normal file
40
migrations/Version20260213114000.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213114000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Allow only one bovin_shipment row per shipment.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Keep one row per shipment (latest id), required before adding unique index.
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DELETE FROM bovin_shipment bs
|
||||||
|
USING (
|
||||||
|
SELECT id, ROW_NUMBER() OVER (PARTITION BY shipment_id ORDER BY id DESC) AS rn
|
||||||
|
FROM bovin_shipment
|
||||||
|
WHERE shipment_id IS NOT NULL
|
||||||
|
) d
|
||||||
|
WHERE bs.id = d.id
|
||||||
|
AND d.rn > 1
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_bovin_shipment');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment_one_type ON bovin_shipment (shipment_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP INDEX IF EXISTS uniq_bovin_shipment_one_type');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment ON bovin_shipment (shipment_id, shipment_type_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,15 @@ declare(strict_types=1);
|
|||||||
namespace App\Command;
|
namespace App\Command;
|
||||||
|
|
||||||
use App\Entity\Address;
|
use App\Entity\Address;
|
||||||
|
use App\Entity\BovineType;
|
||||||
use App\Entity\Building;
|
use App\Entity\Building;
|
||||||
use App\Entity\Carrier;
|
use App\Entity\Carrier;
|
||||||
|
use App\Entity\Customer;
|
||||||
use App\Entity\Driver;
|
use App\Entity\Driver;
|
||||||
use App\Entity\MerchandiseType;
|
use App\Entity\MerchandiseType;
|
||||||
use App\Entity\PelletType;
|
use App\Entity\PelletType;
|
||||||
use App\Entity\ReceptionType;
|
use App\Entity\ReceptionType;
|
||||||
|
use App\Entity\ShipmentType;
|
||||||
use App\Entity\Supplier;
|
use App\Entity\Supplier;
|
||||||
use App\Entity\Truck;
|
use App\Entity\Truck;
|
||||||
use App\Entity\Vehicle;
|
use App\Entity\Vehicle;
|
||||||
@@ -50,7 +53,11 @@ class SeedCommand extends Command
|
|||||||
$this->seedPelletTypes();
|
$this->seedPelletTypes();
|
||||||
$this->seedBuildings();
|
$this->seedBuildings();
|
||||||
$this->seedReceptionTypes();
|
$this->seedReceptionTypes();
|
||||||
|
$this->seedBovineTypes();
|
||||||
|
$this->seedShipmentTypes();
|
||||||
$this->seedSuppliers();
|
$this->seedSuppliers();
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->seedCustomers($io);
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
@@ -61,7 +68,7 @@ class SeedCommand extends Command
|
|||||||
|
|
||||||
private function seedTrucks(): array
|
private function seedTrucks(): array
|
||||||
{
|
{
|
||||||
$trucks = ['Citerne', 'Porteur'];
|
$trucks = ['Citerne', 'Porteur', 'Plateau', 'Remorque', 'Benne'];
|
||||||
$citerne = null;
|
$citerne = null;
|
||||||
$porteur = null;
|
$porteur = null;
|
||||||
foreach ($trucks as $name) {
|
foreach ($trucks as $name) {
|
||||||
@@ -161,6 +168,7 @@ class SeedCommand extends Command
|
|||||||
['label' => 'Foin', 'code' => 'FOIN'],
|
['label' => 'Foin', 'code' => 'FOIN'],
|
||||||
['label' => 'Paille', 'code' => 'PAILLE'],
|
['label' => 'Paille', 'code' => 'PAILLE'],
|
||||||
['label' => 'Granule', 'code' => 'GRANULE'],
|
['label' => 'Granule', 'code' => 'GRANULE'],
|
||||||
|
['label' => 'Autres', 'code' => 'AUTRES'],
|
||||||
];
|
];
|
||||||
foreach ($merchandiseTypes as $type) {
|
foreach ($merchandiseTypes as $type) {
|
||||||
$this->upsertByCode(MerchandiseType::class, $type['code'], static function (MerchandiseType $entity) use ($type) {
|
$this->upsertByCode(MerchandiseType::class, $type['code'], static function (MerchandiseType $entity) use ($type) {
|
||||||
@@ -223,6 +231,39 @@ class SeedCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function seedBovineTypes(): void
|
||||||
|
{
|
||||||
|
$bovineTypes = [
|
||||||
|
['label' => 'Limousine', 'code' => '34'],
|
||||||
|
['label' => 'Charolaise', 'code' => '38'],
|
||||||
|
['label' => 'Parthenaise', 'code' => '71'],
|
||||||
|
];
|
||||||
|
foreach ($bovineTypes as $type) {
|
||||||
|
$this->upsertByCode(BovineType::class, $type['code'], static function (BovineType $entity) use ($type) {
|
||||||
|
$entity
|
||||||
|
->setLabel($type['label'])
|
||||||
|
->setCode($type['code'])
|
||||||
|
;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedShipmentTypes(): void
|
||||||
|
{
|
||||||
|
$shipmentTypes = [
|
||||||
|
['label' => 'Boucherie', 'code' => 'BDB'],
|
||||||
|
['label' => 'Équarrissage', 'code' => 'BE'],
|
||||||
|
];
|
||||||
|
foreach ($shipmentTypes as $type) {
|
||||||
|
$this->upsertByCode(ShipmentType::class, $type['code'], static function (ShipmentType $entity) use ($type) {
|
||||||
|
$entity
|
||||||
|
->setLabel($type['label'])
|
||||||
|
->setCode($type['code'])
|
||||||
|
;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function seedSuppliers(): void
|
private function seedSuppliers(): void
|
||||||
{
|
{
|
||||||
$suppliers = [
|
$suppliers = [
|
||||||
@@ -458,6 +499,130 @@ class SeedCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function seedCustomers(SymfonyStyle $io): void
|
||||||
|
{
|
||||||
|
$addressRepo = $this->entityManager->getRepository(Address::class);
|
||||||
|
$customers = [
|
||||||
|
[
|
||||||
|
'name' => 'ARNAULT EURL',
|
||||||
|
'phone' => '05.49.02.65.27',
|
||||||
|
'email' => 'eurl.arnault86@orange.fr',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'ARNAULT EURL',
|
||||||
|
'street' => 'Moulin du Guéret',
|
||||||
|
'street2' => 'B.P 30425',
|
||||||
|
'postalCode' => '86100',
|
||||||
|
'city' => 'Antran',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'COVILIM',
|
||||||
|
'phone' => '05.55.30.03.10',
|
||||||
|
'email' => 'sandra.robineaux@covilim.com',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'COVILIM',
|
||||||
|
'street' => 'Rue de Nexon',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '87000',
|
||||||
|
'city' => 'LIMOGES',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Les producteurs de la marche (LPM)',
|
||||||
|
'phone' => '05.55.63.04.53',
|
||||||
|
'email' => 'f.legalliard@lpmcoop.fr',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'Les producteurs de la marche (LPM)',
|
||||||
|
'street' => 'Malonze',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '23300',
|
||||||
|
'city' => 'LA SOUTERRAINE',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'LORTHOLARY BETAIL',
|
||||||
|
'phone' => '05.49.52.77.10',
|
||||||
|
'email' => 'contact86@lortholarybetail.com',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'LORTHOLARY BETAIL',
|
||||||
|
'street' => 'FERME DE GENIEC',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '86550',
|
||||||
|
'city' => 'MIGNALOUX BEAUVOIR',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TERRENA',
|
||||||
|
'phone' => '02.40.98.90.00',
|
||||||
|
'email' => 'scouillaud@terrena.fr',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'TERRENA',
|
||||||
|
'street' => 'LA NOELLE',
|
||||||
|
'street2' => 'BP 20199',
|
||||||
|
'postalCode' => '44155',
|
||||||
|
'city' => 'ANCENIS CEDEX',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($customers as $customerData) {
|
||||||
|
$customerName = $customerData['name'] ?? $customerData['label'] ?? null;
|
||||||
|
if (!$customerName) {
|
||||||
|
$io->warning('Customer skipped: missing "name".');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$customer = $this->upsertByName(Customer::class, $customerName, static function (Customer $customer) use ($customerData, $customerName) {
|
||||||
|
$customer
|
||||||
|
->setName($customerName)
|
||||||
|
->setPhone($customerData['phone'] ?? null)
|
||||||
|
->setEmail($customerData['email'] ?? null)
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
|
$addresses = [];
|
||||||
|
if (isset($customerData['addresses']) && is_array($customerData['addresses'])) {
|
||||||
|
foreach ($customerData['addresses'] as $addressData) {
|
||||||
|
$addresses[] = $this->upsertAddress($addressData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Backward compatibility for older seed format with address ids.
|
||||||
|
$addressIds = $customerData['addressIds'] ?? (isset($customerData['addressId']) ? [$customerData['addressId']] : []);
|
||||||
|
foreach ($addressIds as $addressId) {
|
||||||
|
$address = $addressRepo->find($addressId);
|
||||||
|
if (!$address instanceof Address) {
|
||||||
|
$io->warning(sprintf(
|
||||||
|
'Customer "%s" skipped address id %d: not found.',
|
||||||
|
$customerName,
|
||||||
|
$addressId
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$addresses[] = $address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer->setAddresses($addresses);
|
||||||
|
$this->entityManager->persist($customer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function upsertByCode(string $entityClass, string $code, callable $apply): object
|
private function upsertByCode(string $entityClass, string $code, callable $apply): object
|
||||||
{
|
{
|
||||||
$repo = $this->entityManager->getRepository($entityClass);
|
$repo = $this->entityManager->getRepository($entityClass);
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class AppFixtures extends Fixture implements DependentFixtureInterface
|
|||||||
return [
|
return [
|
||||||
TransportFixtures::class,
|
TransportFixtures::class,
|
||||||
ReferenceFixtures::class,
|
ReferenceFixtures::class,
|
||||||
SupplierFixtures::class,
|
|
||||||
UserFixtures::class,
|
UserFixtures::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ declare(strict_types=1);
|
|||||||
namespace App\DataFixtures;
|
namespace App\DataFixtures;
|
||||||
|
|
||||||
use App\Entity\Address;
|
use App\Entity\Address;
|
||||||
|
use App\Entity\BovineType;
|
||||||
use App\Entity\Building;
|
use App\Entity\Building;
|
||||||
|
use App\Entity\Customer;
|
||||||
use App\Entity\MerchandiseType;
|
use App\Entity\MerchandiseType;
|
||||||
use App\Entity\PelletType;
|
use App\Entity\PelletType;
|
||||||
use App\Entity\ReceptionType;
|
use App\Entity\ReceptionType;
|
||||||
|
use App\Entity\ShipmentType;
|
||||||
use App\Entity\Supplier;
|
use App\Entity\Supplier;
|
||||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
use Doctrine\Persistence\ObjectManager;
|
use Doctrine\Persistence\ObjectManager;
|
||||||
@@ -17,10 +20,13 @@ class ReferenceFixtures extends Fixture
|
|||||||
{
|
{
|
||||||
public function load(ObjectManager $manager): void
|
public function load(ObjectManager $manager): void
|
||||||
{
|
{
|
||||||
|
$addressIndex = [];
|
||||||
|
|
||||||
$merchandiseTypes = [
|
$merchandiseTypes = [
|
||||||
['label' => 'Foin', 'code' => 'FOIN'],
|
['label' => 'Foin', 'code' => 'FOIN'],
|
||||||
['label' => 'Paille', 'code' => 'PAILLE'],
|
['label' => 'Paille', 'code' => 'PAILLE'],
|
||||||
['label' => 'Granule', 'code' => 'GRANULE'],
|
['label' => 'Granule', 'code' => 'GRANULE'],
|
||||||
|
['label' => 'Autres', 'code' => 'AUTRES'],
|
||||||
];
|
];
|
||||||
foreach ($merchandiseTypes as $type) {
|
foreach ($merchandiseTypes as $type) {
|
||||||
$merchandiseType = new MerchandiseType()
|
$merchandiseType = new MerchandiseType()
|
||||||
@@ -69,6 +75,31 @@ class ReferenceFixtures extends Fixture
|
|||||||
$manager->persist($receptionType);
|
$manager->persist($receptionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bovineTypes = [
|
||||||
|
['label' => 'Limousine', 'code' => '34'],
|
||||||
|
['label' => 'Charolaise', 'code' => '38'],
|
||||||
|
['label' => 'Parthenaise', 'code' => '71'],
|
||||||
|
];
|
||||||
|
foreach ($bovineTypes as $type) {
|
||||||
|
$bovineType = new BovineType()
|
||||||
|
->setLabel($type['label'])
|
||||||
|
->setCode($type['code'])
|
||||||
|
;
|
||||||
|
$manager->persist($bovineType);
|
||||||
|
}
|
||||||
|
|
||||||
|
$shipmentTypes = [
|
||||||
|
['label' => 'Bovin de boucherie', 'code' => 'BDB'],
|
||||||
|
['label' => "Bovin d'équarrissage", 'code' => 'BE'],
|
||||||
|
];
|
||||||
|
foreach ($shipmentTypes as $type) {
|
||||||
|
$shipmentType = new ShipmentType()
|
||||||
|
->setLabel($type['label'])
|
||||||
|
->setCode($type['code'])
|
||||||
|
;
|
||||||
|
$manager->persist($shipmentType);
|
||||||
|
}
|
||||||
|
|
||||||
$suppliers = [
|
$suppliers = [
|
||||||
[
|
[
|
||||||
'name' => 'LIOT',
|
'name' => 'LIOT',
|
||||||
@@ -290,21 +321,129 @@ class ReferenceFixtures extends Fixture
|
|||||||
;
|
;
|
||||||
|
|
||||||
foreach ($supplierData['addresses'] as $addressData) {
|
foreach ($supplierData['addresses'] as $addressData) {
|
||||||
$address = new Address()
|
$addressKey = sprintf('%s|%s', $addressData['label'], $addressData['postalCode']);
|
||||||
->setLabel($addressData['label'])
|
if (!isset($addressIndex[$addressKey])) {
|
||||||
->setStreet($addressData['street'])
|
$addressIndex[$addressKey] = new Address()
|
||||||
->setStreet2($addressData['street2'])
|
->setLabel($addressData['label'])
|
||||||
->setPostalCode($addressData['postalCode'])
|
->setStreet($addressData['street'])
|
||||||
->setCity($addressData['city'])
|
->setStreet2($addressData['street2'])
|
||||||
->setCountryCode($addressData['countryCode'])
|
->setPostalCode($addressData['postalCode'])
|
||||||
;
|
->setCity($addressData['city'])
|
||||||
$manager->persist($address);
|
->setCountryCode($addressData['countryCode'])
|
||||||
|
;
|
||||||
|
$manager->persist($addressIndex[$addressKey]);
|
||||||
|
}
|
||||||
|
$address = $addressIndex[$addressKey];
|
||||||
$supplier->getAddresses()->add($address);
|
$supplier->getAddresses()->add($address);
|
||||||
}
|
}
|
||||||
|
|
||||||
$manager->persist($supplier);
|
$manager->persist($supplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$customers = [
|
||||||
|
[
|
||||||
|
'name' => 'ARNAULT EURL',
|
||||||
|
'phone' => '05.49.02.65.27',
|
||||||
|
'email' => 'eurl.arnault86@orange.fr',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'ARNAULT EURL',
|
||||||
|
'street' => 'Moulin du Guéret',
|
||||||
|
'street2' => 'B.P 30425',
|
||||||
|
'postalCode' => '86100',
|
||||||
|
'city' => 'Antran',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'COVILIM',
|
||||||
|
'phone' => '05.55.30.03.10',
|
||||||
|
'email' => 'sandra.robineaux@covilim.com',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'COVILIM',
|
||||||
|
'street' => 'Rue de Nexon',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '87000',
|
||||||
|
'city' => 'LIMOGES',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Les producteurs de la marche (LPM)',
|
||||||
|
'phone' => '05.55.63.04.53',
|
||||||
|
'email' => 'f.legalliard@lpmcoop.fr',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'Les producteurs de la marche (LPM)',
|
||||||
|
'street' => 'Malonze',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '23300',
|
||||||
|
'city' => 'LA SOUTERRAINE',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'LORTHOLARY BETAIL',
|
||||||
|
'phone' => '05.49.52.77.10',
|
||||||
|
'email' => 'contact86@lortholarybetail.com',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'LORTHOLARY BETAIL',
|
||||||
|
'street' => 'FERME DE GENIEC',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '86550',
|
||||||
|
'city' => 'MIGNALOUX BEAUVOIR',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'TERRENA',
|
||||||
|
'phone' => '02.40.98.90.00',
|
||||||
|
'email' => 'scouillaud@terrena.fr',
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'TERRENA',
|
||||||
|
'street' => 'LA NOELLE',
|
||||||
|
'street2' => 'BP 20199',
|
||||||
|
'postalCode' => '44155',
|
||||||
|
'city' => 'ANCENIS CEDEX',
|
||||||
|
'countryCode' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($customers as $customerData) {
|
||||||
|
$customer = new Customer()
|
||||||
|
->setName($customerData['name'])
|
||||||
|
->setPhone($customerData['phone'])
|
||||||
|
->setEmail($customerData['email'])
|
||||||
|
;
|
||||||
|
|
||||||
|
foreach ($customerData['addresses'] as $addressData) {
|
||||||
|
$addressKey = sprintf('%s|%s', $addressData['label'], $addressData['postalCode']);
|
||||||
|
if (!isset($addressIndex[$addressKey])) {
|
||||||
|
$addressIndex[$addressKey] = new Address()
|
||||||
|
->setLabel($addressData['label'])
|
||||||
|
->setStreet($addressData['street'])
|
||||||
|
->setStreet2($addressData['street2'])
|
||||||
|
->setPostalCode($addressData['postalCode'])
|
||||||
|
->setCity($addressData['city'])
|
||||||
|
->setCountryCode($addressData['countryCode'])
|
||||||
|
;
|
||||||
|
$manager->persist($addressIndex[$addressKey]);
|
||||||
|
}
|
||||||
|
$customer->getAddresses()->add($addressIndex[$addressKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->persist($customer);
|
||||||
|
}
|
||||||
|
|
||||||
$manager->flush();
|
$manager->flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\DataFixtures;
|
|
||||||
|
|
||||||
use App\Entity\Address;
|
|
||||||
use App\Entity\Supplier;
|
|
||||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
|
||||||
use Doctrine\Persistence\ObjectManager;
|
|
||||||
|
|
||||||
class SupplierFixtures extends Fixture
|
|
||||||
{
|
|
||||||
public function load(ObjectManager $manager): void
|
|
||||||
{
|
|
||||||
$address = new Address()
|
|
||||||
->setLabel('LIOT CHATELLERAULT')
|
|
||||||
->setStreet("14 Allée d'Argenson")
|
|
||||||
->setStreet2('ZI Nord')
|
|
||||||
->setPostalCode('86100')
|
|
||||||
->setCity('CHATELLERAULT')
|
|
||||||
->setCountryCode('FR')
|
|
||||||
;
|
|
||||||
|
|
||||||
$supplier = new Supplier()
|
|
||||||
->setName('LIOT')
|
|
||||||
->setEmail('lpc.contacts@lpc-liot.fr')
|
|
||||||
->setPhone('05.49.20.09.10')
|
|
||||||
;
|
|
||||||
|
|
||||||
$supplier->getAddresses()->add($address);
|
|
||||||
|
|
||||||
$manager->persist($address);
|
|
||||||
$manager->persist($supplier);
|
|
||||||
|
|
||||||
$manager->flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,11 +15,17 @@ class TransportFixtures extends Fixture
|
|||||||
{
|
{
|
||||||
public function load(ObjectManager $manager): void
|
public function load(ObjectManager $manager): void
|
||||||
{
|
{
|
||||||
$citerne = new Truck()->setName('Citerne');
|
$citerne = new Truck()->setName('Citerne');
|
||||||
$porteur = new Truck()->setName('Porteur');
|
$porteur = new Truck()->setName('Porteur');
|
||||||
|
$plateau = new Truck()->setName('Plateau');
|
||||||
|
$remorque = new Truck()->setName('Remorque');
|
||||||
|
$benne = new Truck()->setName('Benne');
|
||||||
|
|
||||||
$manager->persist($citerne);
|
$manager->persist($citerne);
|
||||||
$manager->persist($porteur);
|
$manager->persist($porteur);
|
||||||
|
$manager->persist($plateau);
|
||||||
|
$manager->persist($remorque);
|
||||||
|
$manager->persist($benne);
|
||||||
|
|
||||||
$liot = new Carrier()
|
$liot = new Carrier()
|
||||||
->setName('LIOT')
|
->setName('LIOT')
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
|
|
||||||
#[ORM\Entity]
|
#[ORM\Entity]
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['shipment' => 'exact'])]
|
#[ApiFilter(SearchFilter::class, properties: ['shipment' => 'exact'])]
|
||||||
#[ORM\UniqueConstraint(name: 'uniq_bovin_shipment', columns: ['shipment_id', 'shipment_type_id'])]
|
#[ORM\UniqueConstraint(name: 'uniq_bovin_shipment_one_type', columns: ['shipment_id'])]
|
||||||
#[ORM\Table(name: 'bovin_shipment')]
|
#[ORM\Table(name: 'bovin_shipment')]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
operations: [
|
operations: [
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
new Get(
|
new Get(
|
||||||
requirements: ['id' => '\d+'],
|
requirements: ['id' => '\d+'],
|
||||||
normalizationContext: ['groups' => ['carrier:read']],
|
normalizationContext: ['groups' => ['carrier:read']],
|
||||||
|
security: "is_granted('ROLE_USER')"
|
||||||
),
|
),
|
||||||
new GetCollection(
|
new GetCollection(
|
||||||
normalizationContext: ['groups' => ['carrier:read']],
|
normalizationContext: ['groups' => ['carrier:read']],
|
||||||
|
security: "is_granted('ROLE_USER')"
|
||||||
),
|
),
|
||||||
new Post(
|
new Post(
|
||||||
normalizationContext: ['groups' => ['carrier:read']],
|
normalizationContext: ['groups' => ['carrier:read']],
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use ApiPlatform\Metadata\ApiProperty;
|
|||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@@ -24,6 +26,16 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
new GetCollection(
|
new GetCollection(
|
||||||
normalizationContext: ['groups' => ['customer:read']],
|
normalizationContext: ['groups' => ['customer:read']],
|
||||||
),
|
),
|
||||||
|
new Post(
|
||||||
|
normalizationContext: ['groups' => ['customer:read']],
|
||||||
|
denormalizationContext: ['groups' => ['customer:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
normalizationContext: ['groups' => ['customer:read']],
|
||||||
|
denormalizationContext: ['groups' => ['customer:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('ROLE_USER')",
|
||||||
)]
|
)]
|
||||||
@@ -35,20 +47,24 @@ class Customer
|
|||||||
#[Groups(['shipment:read', 'customer:read'])]
|
#[Groups(['shipment:read', 'customer:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['customer:read', 'shipment:read'])]
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
private ?string $label = null;
|
private string $name = '';
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 180, nullable: true)]
|
||||||
#[Groups(['customer:read', 'shipment:read'])]
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
private ?string $code = null;
|
private ?string $email = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 40, nullable: true)]
|
||||||
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
|
private ?string $phone = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Address>
|
* @var Collection<int, Address>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'customers')]
|
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'customers')]
|
||||||
#[ORM\JoinTable(name: 'customer_address')]
|
#[ORM\JoinTable(name: 'customer_address')]
|
||||||
#[Groups(['customer:read'])]
|
#[Groups(['customer:read', 'customer:write'])]
|
||||||
#[ApiProperty(readableLink: true)]
|
#[ApiProperty(readableLink: true)]
|
||||||
private Collection $addresses;
|
private Collection $addresses;
|
||||||
|
|
||||||
@@ -62,24 +78,40 @@ class Customer
|
|||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabel(): ?string
|
public function getName(): string
|
||||||
{
|
{
|
||||||
return $this->label;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setLabel(?string $label): void
|
public function setName(string $name): self
|
||||||
{
|
{
|
||||||
$this->label = $label;
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCode(): ?string
|
public function getEmail(): ?string
|
||||||
{
|
{
|
||||||
return $this->code;
|
return $this->email;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCode(?string $code): void
|
public function setEmail(?string $email): self
|
||||||
{
|
{
|
||||||
$this->code = $code;
|
$this->email = $email;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPhone(): ?string
|
||||||
|
{
|
||||||
|
return $this->phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPhone(?string $phone): self
|
||||||
|
{
|
||||||
|
$this->phone = $phone;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAddresses(): Collection
|
public function getAddresses(): Collection
|
||||||
@@ -87,8 +119,29 @@ class Customer
|
|||||||
return $this->addresses;
|
return $this->addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAddresses(Collection $addresses): void
|
public function setAddresses(iterable $addresses): self
|
||||||
{
|
{
|
||||||
$this->addresses = $addresses;
|
$this->addresses->clear();
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$this->addAddress($address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addAddress(Address $address): self
|
||||||
|
{
|
||||||
|
if (!$this->addresses->contains($address)) {
|
||||||
|
$this->addresses->add($address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeAddress(Address $address): self
|
||||||
|
{
|
||||||
|
$this->addresses->removeElement($address);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ class Shipment
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$number = sprintf('P-BR-%04d', $this->id);
|
$number = sprintf('P-BL-%04d', $this->id);
|
||||||
$this->identificationNumber = $number;
|
$this->identificationNumber = $number;
|
||||||
|
|
||||||
$args->getObjectManager()
|
$args->getObjectManager()
|
||||||
|
|||||||
@@ -22,14 +22,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
new Get(
|
new Get(
|
||||||
requirements: ['id' => '\d+'],
|
requirements: ['id' => '\d+'],
|
||||||
normalizationContext: ['groups' => ['supplier:read']],
|
normalizationContext: ['groups' => ['supplier:read']],
|
||||||
|
security: "is_granted('ROLE_USER')"
|
||||||
),
|
),
|
||||||
new GetCollection(
|
new GetCollection(
|
||||||
normalizationContext: ['groups' => ['supplier:read']],
|
normalizationContext: ['groups' => ['supplier:read']],
|
||||||
),
|
security: "is_granted('ROLE_USER')"
|
||||||
new GetCollection(
|
|
||||||
uriTemplate: '/admin/suppliers',
|
|
||||||
normalizationContext: ['groups' => ['supplier:read']],
|
|
||||||
security: "is_granted('ROLE_ADMIN')"
|
|
||||||
),
|
),
|
||||||
new Post(
|
new Post(
|
||||||
normalizationContext: ['groups' => ['supplier:read']],
|
normalizationContext: ['groups' => ['supplier:read']],
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ final class UserPasswordProcessor implements ProcessorInterface
|
|||||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||||
{
|
{
|
||||||
if ($data instanceof User) {
|
if ($data instanceof User) {
|
||||||
$plain = $data->getPassword();
|
$plain = $data->getPassword();
|
||||||
if ('' !== $plain) {
|
$previous = $context['previous_data'] ?? null;
|
||||||
|
if ($previous instanceof User && $plain === $previous->getPassword()) {
|
||||||
|
// Password not changed in payload: keep existing hash.
|
||||||
|
$data->setPassword($previous->getPassword());
|
||||||
|
} elseif ('' !== $plain) {
|
||||||
$data->setPassword($this->hasher->hashPassword(
|
$data->setPassword($this->hasher->hashPassword(
|
||||||
$data,
|
$data,
|
||||||
$plain
|
$plain
|
||||||
|
|||||||
@@ -174,7 +174,7 @@
|
|||||||
|
|
||||||
<td style="width:30%; text-align:right; vertical-align:top; font-size: 14px;">
|
<td style="width:30%; text-align:right; vertical-align:top; font-size: 14px;">
|
||||||
<div style="display:inline-block; width:75mm; line-height:1.3;">
|
<div style="display:inline-block; width:75mm; line-height:1.3;">
|
||||||
<strong>{{ shipment.customer ? shipment.customer.label : '-' }}</strong><br>
|
<strong>{{ shipment.customer ? shipment.customer.name : '-' }}</strong><br>
|
||||||
<span>{{ shipment.address ? shipment.address.street : '' }}</span><br>
|
<span>{{ shipment.address ? shipment.address.street : '' }}</span><br>
|
||||||
{% if shipment.address and shipment.address.street2 %}
|
{% if shipment.address and shipment.address.street2 %}
|
||||||
<span>{{ shipment.address.street2 }}</span><br>
|
<span>{{ shipment.address.street2 }}</span><br>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:55%; text-align:center;">
|
<td style="width:55%; text-align:center;">
|
||||||
{{ shipment.customer ? shipment.customer.code : '-' }}
|
{{ shipment.customer ? shipment.customer.name : '-' }}
|
||||||
</td>
|
</td>
|
||||||
<td style="width:20%; text-align:center; white-space:nowrap;">
|
<td style="width:20%; text-align:center; white-space:nowrap;">
|
||||||
{{ shipment.shipmentDate|date('d/m/Y') }}
|
{{ shipment.shipmentDate|date('d/m/Y') }}
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
<td style="width:60%; padding-right:8mm; vertical-align:top;">
|
<td style="width:60%; padding-right:8mm; vertical-align:top;">
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<p>Transporteur : {{ shipment.carrier ? shipment.carrier.name : '-' }}</p>
|
<p>Transporteur : {{ shipment.carrier ? shipment.carrier.name : '-' }}</p>
|
||||||
<p>Mode de livraison : {{ shipment.truck ? shipment.truck.name : '-' }}</p>
|
<p>Mode d'expédition : {{ shipment.truck ? shipment.truck.name : '-' }}</p>
|
||||||
<p>Immatriculation : {{ shipment.licencePlate ?? '-' }}</p>
|
<p>Immatriculation : {{ shipment.licencePlate ?? '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user