Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f58dc36a0d | ||
| 15c0f414af | |||
|
|
9ed0ba702e | ||
| 93edd0a563 | |||
|
|
c361ef9bb9 | ||
| 7f3d9ef9c6 | |||
|
|
22b959de85 | ||
| d3bc2e11f1 | |||
|
|
d8b16f5e15 | ||
| 43213bc6d6 | |||
|
|
09666d9319 | ||
| 05ea33735d | |||
|
|
89c67f7e97 | ||
| d527e94bac | |||
|
|
579bdba65b | ||
| b1c3952d09 |
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>
|
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="PhpCSFixerValidationInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
1
.idea/php.xml
generated
1
.idea/php.xml
generated
@@ -15,6 +15,7 @@
|
|||||||
<component name="PhpCSFixer">
|
<component name="PhpCSFixer">
|
||||||
<phpcsfixer_settings>
|
<phpcsfixer_settings>
|
||||||
<PhpCSFixerConfiguration tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
|
<PhpCSFixerConfiguration tool_path="$PROJECT_DIR$/vendor/bin/php-cs-fixer" />
|
||||||
|
<phpcs_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="990ff521-e6e9-4080-9cc9-228367d597f9" tool_path="\\wsl.localhost\Ubuntu-24.04\home\matte\Ferme\vendor\bin\php-cs-fixer" timeout="30000" />
|
||||||
</phpcsfixer_settings>
|
</phpcsfixer_settings>
|
||||||
</component>
|
</component>
|
||||||
<component name="PhpCodeSniffer">
|
<component name="PhpCodeSniffer">
|
||||||
|
|||||||
203
.idea/workspace.xml
generated
203
.idea/workspace.xml
generated
@@ -4,11 +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="fix : panel scrollable plus interface revue">
|
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : corrections diverses">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/data_source_mapping.xml" beforeDir="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/pages/admin/carrier/carrier-list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/carrier/carrier-list.vue" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" afterDir="false" />
|
|
||||||
</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" />
|
||||||
@@ -33,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="feat/256-reception-etape-3-bovin" />
|
<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 />
|
||||||
@@ -220,43 +223,48 @@
|
|||||||
<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/312-creation-d-une-page-d-administration-listing-des-fournisseurs",
|
"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\matte\Ferme\frontend\pages\admin\supplier" />
|
|
||||||
<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" />
|
||||||
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||||
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
||||||
|
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\frontend\pages\reception" />
|
||||||
</key>
|
</key>
|
||||||
</component>
|
</component>
|
||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
@@ -296,6 +304,34 @@
|
|||||||
<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" />
|
||||||
|
<workItem from="1770798536017" duration="20774000" />
|
||||||
|
<workItem from="1770879701502" duration="25805000" />
|
||||||
|
<workItem from="1770966186589" duration="914000" />
|
||||||
|
<workItem from="1770967274060" duration="2388000" />
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1768318921478</created>
|
||||||
|
<option name="number" value="00007" />
|
||||||
|
<option name="presentableId" value="LOCAL-00007" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1768318921478</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00008" summary="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1768498751836</created>
|
||||||
|
<option name="number" value="00008" />
|
||||||
|
<option name="presentableId" value="LOCAL-00008" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1768498751836</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00009" summary="feat : ajout d'une gestion d'erreur au global côté front avec la lib toaster et I18n pour centraliser les messages d'erreur">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1768555180530</created>
|
||||||
|
<option name="number" value="00009" />
|
||||||
|
<option name="presentableId" value="LOCAL-00009" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1768555180530</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik">
|
<task id="LOCAL-00010" summary="feat : ajout de l'authentification avec lexik">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -617,79 +653,55 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770217875423</updated>
|
<updated>1770217875423</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00050" summary="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
<task id="LOCAL-00050" summary="feat : creer une nouvelle expedtion (WIP)">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770283622425</created>
|
<created>1770736570645</created>
|
||||||
<option name="number" value="00050" />
|
<option name="number" value="00050" />
|
||||||
<option name="presentableId" value="LOCAL-00050" />
|
<option name="presentableId" value="LOCAL-00050" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770283622425</updated>
|
<updated>1770736570645</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00051" summary="feat : ajout du responsive sur la navbar et la page d'accueil">
|
<task id="LOCAL-00051" summary="feat : ajout d'une page de creation d'une expedition">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770308927948</created>
|
<created>1770880791564</created>
|
||||||
<option name="number" value="00051" />
|
<option name="number" value="00051" />
|
||||||
<option name="presentableId" value="LOCAL-00051" />
|
<option name="presentableId" value="LOCAL-00051" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770308927948</updated>
|
<updated>1770880791565</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00052" summary="fix : logo centré en mod mobile">
|
<task id="LOCAL-00052" summary="feat : changelog">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770310504254</created>
|
<created>1770881437439</created>
|
||||||
<option name="number" value="00052" />
|
<option name="number" value="00052" />
|
||||||
<option name="presentableId" value="LOCAL-00052" />
|
<option name="presentableId" value="LOCAL-00052" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770310504254</updated>
|
<updated>1770881437439</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00053" summary="feat : ajout d'un numéro de version automatique via la CI">
|
<task id="LOCAL-00053" summary="feat : lister les expeditions terminees">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770369945257</created>
|
<created>1770883114609</created>
|
||||||
<option name="number" value="00053" />
|
<option name="number" value="00053" />
|
||||||
<option name="presentableId" value="LOCAL-00053" />
|
<option name="presentableId" value="LOCAL-00053" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770369945257</updated>
|
<updated>1770883114609</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00054" summary="feat : update numéro de version">
|
<task id="LOCAL-00054" summary="feat : lister les expeditions terminees">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770370216428</created>
|
<created>1770884154297</created>
|
||||||
<option name="number" value="00054" />
|
<option name="number" value="00054" />
|
||||||
<option name="presentableId" value="LOCAL-00054" />
|
<option name="presentableId" value="LOCAL-00054" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770370216428</updated>
|
<updated>1770884154297</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00055" summary="fix : auto-tag-develop.yml">
|
<task id="LOCAL-00055" summary="fix : corrections diverses">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
<created>1770370700697</created>
|
<created>1770969471135</created>
|
||||||
<option name="number" value="00055" />
|
<option name="number" value="00055" />
|
||||||
<option name="presentableId" value="LOCAL-00055" />
|
<option name="presentableId" value="LOCAL-00055" />
|
||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1770370700698</updated>
|
<updated>1770969471135</updated>
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00056" summary="fix : auto-tag-develop.yml">
|
<option name="localTasksCounter" value="56" />
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770370919043</created>
|
|
||||||
<option name="number" value="00056" />
|
|
||||||
<option name="presentableId" value="LOCAL-00056" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770370919043</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00057" summary="feat : test auto-tag-develop.yml (auto incrément version)">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770371073055</created>
|
|
||||||
<option name="number" value="00057" />
|
|
||||||
<option name="presentableId" value="LOCAL-00057" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770371073055</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00058" summary="fix : nom page fournisseur">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1770632525875</created>
|
|
||||||
<option name="number" value="00058" />
|
|
||||||
<option name="presentableId" value="LOCAL-00058" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1770632525875</updated>
|
|
||||||
</task>
|
|
||||||
<option name="localTasksCounter" value="59" />
|
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -739,6 +751,7 @@
|
|||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
|
<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" />
|
||||||
<MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" />
|
<MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" />
|
||||||
@@ -757,14 +770,38 @@
|
|||||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||||
<MESSAGE value="feat : mise à jour du bon de réception" />
|
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||||
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||||
<MESSAGE value="feat : ajout du responsive sur la navbar et la page d'accueil" />
|
<MESSAGE value="feat : creer une nouvelle expedtion (WIP)" />
|
||||||
<MESSAGE value="fix : logo centré en mod mobile" />
|
<MESSAGE value="feat : ajout d'une page de creation d'une expedition" />
|
||||||
<MESSAGE value="feat : ajout d'un numéro de version automatique via la CI" />
|
<MESSAGE value="feat : changelog" />
|
||||||
<MESSAGE value="feat : update numéro de version" />
|
<MESSAGE value="feat : lister les expeditions terminees" />
|
||||||
<MESSAGE value="fix : auto-tag-develop.yml" />
|
<MESSAGE value="fix: corrections diverses" />
|
||||||
<MESSAGE value="feat : test auto-tag-develop.yml (auto incrément version)" />
|
<MESSAGE value="fix : corrections diverses" />
|
||||||
<MESSAGE value="fix : nom page fournisseur" />
|
<option name="LAST_COMMIT_MESSAGE" value="fix : corrections diverses" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="fix : nom page fournisseur" />
|
</component>
|
||||||
|
<component name="XDebuggerManager">
|
||||||
|
<breakpoint-manager>
|
||||||
|
<breakpoints>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/Entity/ReceptionPelletBuilding.php</url>
|
||||||
|
<line>6</line>
|
||||||
|
<option name="timeStamp" value="3" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="php">
|
||||||
|
<url>file://$PROJECT_DIR$/src/Entity/Shipment.php</url>
|
||||||
|
<line>6</line>
|
||||||
|
<option name="timeStamp" value="45" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<line-breakpoint enabled="true" type="javascript">
|
||||||
|
<url>file://$PROJECT_DIR$/frontend/services/dto/shipment-data.ts</url>
|
||||||
|
<option name="timeStamp" value="43" />
|
||||||
|
</line-breakpoint>
|
||||||
|
<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>
|
||||||
|
</breakpoints>
|
||||||
|
</breakpoint-manager>
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
@@ -778,4 +815,4 @@
|
|||||||
<option value=".github/prompts" />
|
<option value=".github/prompts" />
|
||||||
</promptFileLocations>
|
</promptFileLocations>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -35,6 +35,17 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* [#317] Admin modification creation transporteur
|
* [#317] Admin modification creation transporteur
|
||||||
* [#318] Affichage modification reception terminée
|
* [#318] Affichage modification reception terminée
|
||||||
* [#320] Affichage modification reception terminée suite
|
* [#320] Affichage modification reception terminée suite
|
||||||
|
* [#271] Créer une nouvelle expédition (étape 1)
|
||||||
|
* [#272] Créer une nouvelle expédition (étape 2)
|
||||||
|
* [#273] Créer une nouvelle expédition (étape 3)
|
||||||
|
* [#256] Créer une nouvelle réception (étape 3 - bovin)
|
||||||
|
* [#314] Création d'une page d'administration : listing des utilisateurs
|
||||||
|
* [#313] Admin modification creation fournisseur
|
||||||
|
* [#275] Lister les expéditions en attente
|
||||||
|
* [#276] Lister les expéditions terminées
|
||||||
|
* [#324] Creation page admin listing clients
|
||||||
|
* [#326] Admin modification creation client
|
||||||
|
* [#325] Correction diverses
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.0.38'
|
app.version: '0.0.46'
|
||||||
|
|||||||
81
frontend/components/address.vue
Normal file
81
frontend/components/address.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validateForm">
|
||||||
|
<div class="flex items-center justify-between gap-10">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold uppercase">
|
||||||
|
{{ props.address ? "Modification d'une adresse" : "Ajout d'une adresse" }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
{{ props.address? "Sauvegarder" : "Ajouter" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-y-16 gap-x-12 mb-16 mt-10">
|
||||||
|
<UiTextInput id="address-label" v-model="form.label" label="Libellé" />
|
||||||
|
<UiTextInput id="address-street" v-model="form.street" label="Rue" />
|
||||||
|
<UiTextInput id="address-street2" v-model="form.street2" label="Complément" />
|
||||||
|
<UiTextInput id="address-postalCode" v-model="form.postalCode" label="Code postal" />
|
||||||
|
<UiTextInput id="address-city" v-model="form.city" label="Ville" />
|
||||||
|
<UiTextInput id="address-country" v-model="form.countryCode" label="Pays" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { AddressPayload } from "~/services/address"
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type?: "supplier" | "customer",
|
||||||
|
address?: AddressPayload | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const emptyForm = (): AddressPayload => ({
|
||||||
|
label: "",
|
||||||
|
street: "",
|
||||||
|
street2: null,
|
||||||
|
postalCode: "",
|
||||||
|
city: "",
|
||||||
|
countryCode: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive<AddressPayload>(emptyForm())
|
||||||
|
|
||||||
|
const hydrateForm = (address?: AddressPayload | null) => {
|
||||||
|
const data = address ?? emptyForm()
|
||||||
|
form.label = data.label ?? ""
|
||||||
|
form.street = data.street ?? ""
|
||||||
|
form.street2 = data.street2 ?? null
|
||||||
|
form.postalCode = data.postalCode ?? ""
|
||||||
|
form.city = data.city ?? ""
|
||||||
|
form.countryCode = data.countryCode ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.address,
|
||||||
|
(addr) => {
|
||||||
|
hydrateForm(addr)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
if (isLoading.value) return
|
||||||
|
emit("validate", {...form})
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'validate', form: AddressPayload): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -29,7 +29,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>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
>Peser
|
>Valider
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -143,7 +143,7 @@ import type {DriverData} from '~/services/dto/driver-data'
|
|||||||
import {getDriverList} from '~/services/driver'
|
import {getDriverList} from '~/services/driver'
|
||||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||||
import {getVehicleList} from '~/services/vehicle'
|
import {getVehicleList} from '~/services/vehicle'
|
||||||
import {RECEPTION_TYPE_CODES, SUPLLIER_CODE} from "~/utils/constants";
|
import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
|
||||||
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
||||||
import type {ReceptionFormData} from "~/services/dto/reception-data";
|
import type {ReceptionFormData} from "~/services/dto/reception-data";
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ const selectedCarrier = computed(() =>
|
|||||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
)
|
)
|
||||||
// Indique si le transporteur est LIOT
|
// Indique si le transporteur est LIOT
|
||||||
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPLLIER_CODE.LIOT)
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
// Adresses disponibles pour le fournisseur sélectionné
|
// Adresses disponibles pour le fournisseur sélectionné
|
||||||
const supplierAddresses = computed(() => {
|
const supplierAddresses = computed(() => {
|
||||||
const supplierId = Number(form.supplierId)
|
const supplierId = Number(form.supplierId)
|
||||||
@@ -342,7 +342,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 +359,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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
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</button>
|
||||||
<button
|
<button
|
||||||
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"
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</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'
|
||||||
@@ -74,7 +74,9 @@ const printReceipt = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await saveWeight()
|
await saveWeight()
|
||||||
await printPdf(`/receptions/${receptionStore.current.id}/receipt`)
|
const reception = receptionStore.current
|
||||||
|
const filename = `${reception.identificationNumber ?? reception.id}_${reception.supplier?.name ?? 'fournisseur'}_${reception.licensePlate ?? 'immat'}.pdf`
|
||||||
|
await printPdf(`/receptions/${reception.id}/receipt`, filename)
|
||||||
|
|
||||||
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
@@ -92,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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<button
|
||||||
|
v-if="auth.isAdmin"
|
||||||
|
type="submit"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="!auth.isAdmin"
|
||||||
|
>Valider
|
||||||
|
</button>
|
||||||
</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,27 +1,36 @@
|
|||||||
<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">
|
<UiTextInput
|
||||||
<UiNumberInput
|
label="Dsd"
|
||||||
label="Pesée à vide"
|
class="col-start-2"
|
||||||
v-model="form.weights[0].weight"
|
v-model="sharedWeightMeta.dsd"
|
||||||
:disabled="!auth.isAdmin"
|
:disabled="!auth.isAdmin"
|
||||||
:min="0"
|
|
||||||
/>
|
/>
|
||||||
|
<UiDateInput
|
||||||
<UiNumberInput
|
label="Date pesée"
|
||||||
label="Pesée à plein"
|
v-model="sharedWeightMeta.weighedAt"
|
||||||
v-model="form.weights[1].weight"
|
:disabled="!auth.isAdmin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-x-40 mb-16">
|
||||||
|
<UiNumberInput
|
||||||
|
v-for="weight in form.weights"
|
||||||
|
:key="weight.type"
|
||||||
|
:label="getWeightLabel(weight.type)"
|
||||||
|
labelClass="font-bold uppercase text-xl"
|
||||||
|
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button
|
<button
|
||||||
|
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>
|
</button>
|
||||||
@@ -32,7 +41,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 +54,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 +98,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>
|
||||||
|
|||||||
609
frontend/components/shipment/shipment-form.vue
Normal file
609
frontend/components/shipment/shipment-form.vue
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="validate">
|
||||||
|
<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>
|
||||||
|
<!-- Nom de l'utilisateur -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-user"
|
||||||
|
v-model="form.userId"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
:options="users.map((user) => ({
|
||||||
|
value: String(user.id),
|
||||||
|
label: user.username
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingUsers"
|
||||||
|
wrapper-class="col-start-1 row-start-2"
|
||||||
|
/>
|
||||||
|
<!-- Date de l'éxpedition -->
|
||||||
|
<UiDateInput
|
||||||
|
id="shipment-date"
|
||||||
|
v-model="form.shipmentDate"
|
||||||
|
label="Date du jour"
|
||||||
|
wrapper-class="col-start-1 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- Type d'expédition -->
|
||||||
|
<div class="col-start-1 row-start-4">
|
||||||
|
<label class="font-bold uppercase text-xl mb-2">
|
||||||
|
Type d'expédition
|
||||||
|
</label>
|
||||||
|
<div class="grid grid-cols-2 gap-x-8">
|
||||||
|
<div
|
||||||
|
v-for="type in bovineShipment"
|
||||||
|
:key="type.id"
|
||||||
|
class="mt-2 flex flex-row gap-6"
|
||||||
|
>
|
||||||
|
<UiNumberInput
|
||||||
|
:label="type.label"
|
||||||
|
v-model="bovineQuantities[String(type.id)]"
|
||||||
|
:placeholder="0"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Client -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-customer"
|
||||||
|
v-model="form.customerId"
|
||||||
|
label="Client"
|
||||||
|
:options="customers.map((customer) => ({
|
||||||
|
value: String(customer.id),
|
||||||
|
label: customer.name || `Client #${customer.id}`
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingCustomers"
|
||||||
|
wrapper-class="col-start-1 row-start-5"
|
||||||
|
/>
|
||||||
|
<!-- Adresse du client -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-address"
|
||||||
|
v-model="form.addressId"
|
||||||
|
:options="customerAddressOptions"
|
||||||
|
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||||
|
label="Adresse"
|
||||||
|
wrapper-class="col-start-2 row-start-1"
|
||||||
|
/>
|
||||||
|
<!-- Camion -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-truck"
|
||||||
|
v-model="form.truckId"
|
||||||
|
label="Camion"
|
||||||
|
:options="trucks.map((truck) => ({
|
||||||
|
value: String(truck.id),
|
||||||
|
label: truck.name
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingTrucks"
|
||||||
|
wrapper-class="col-start-2 row-start-2"
|
||||||
|
/>
|
||||||
|
<!-- Transporteur -->
|
||||||
|
<UiSelect
|
||||||
|
id="shipment-carrier"
|
||||||
|
v-model="form.carrierId"
|
||||||
|
label="Transporteur"
|
||||||
|
:options="carriers.map((carrier) => ({
|
||||||
|
value: String(carrier.id),
|
||||||
|
label: carrier.name
|
||||||
|
}))"
|
||||||
|
wrapper-class="col-start-2 row-start-3"
|
||||||
|
/>
|
||||||
|
<!-- 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) -->
|
||||||
|
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
|
||||||
|
<UiLicensePlateInput
|
||||||
|
v-model="form.licencePlate"
|
||||||
|
v-model:allowAny="allowAnyLicensePlate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Immatriculation (LIOT) -->
|
||||||
|
<UiSelect
|
||||||
|
v-if="isLiotCarrier"
|
||||||
|
id="shipment-vehicle"
|
||||||
|
v-model="form.vehicleId"
|
||||||
|
label="Immatriculation"
|
||||||
|
:options="filteredVehicles.map((vehicle) => ({
|
||||||
|
value: String(vehicle.id),
|
||||||
|
label: vehicle.plate
|
||||||
|
}))"
|
||||||
|
:loading="isLoadingVehicles"
|
||||||
|
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
|
||||||
|
wrapper-class="col-start-2 row-start-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||||
|
>Valider
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {UserData} from '~/services/dto/user-data'
|
||||||
|
import type {CustomerData} from '~/services/dto/customer-data'
|
||||||
|
import type {TruckData} from '~/services/dto/truck-data'
|
||||||
|
import type {CarrierData} from '~/services/dto/carrier-data'
|
||||||
|
import type {DriverData} from '~/services/dto/driver-data'
|
||||||
|
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||||
|
import type {AddressData} from '~/services/dto/address-data'
|
||||||
|
import {getUsers} from '~/services/auth'
|
||||||
|
import {getCustomerList} from '~/services/customer'
|
||||||
|
import {getTruckList} from '~/services/truck'
|
||||||
|
import {getCarrierList} from '~/services/carrier'
|
||||||
|
import {getVehicleList} from '~/services/vehicle'
|
||||||
|
import {getDriverList} from '~/services/driver'
|
||||||
|
import type {ShipmentFormData} from '~/services/dto/shipment-data'
|
||||||
|
import {SUPPLIER_CODE} from "~/utils/constants"
|
||||||
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
import {useShipmentStore} from '~/stores/shipment'
|
||||||
|
import { computed, reactive, ref, watch, onMounted } from 'vue'
|
||||||
|
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
|
||||||
|
import {getShipmentTypeList} from "~/services/shipment-type";
|
||||||
|
import {
|
||||||
|
createShipmentBovine,
|
||||||
|
deleteShipmentBovine,
|
||||||
|
getBovinShipmentList,
|
||||||
|
updateShipmentBovine
|
||||||
|
} from "~/services/bovin-shipment";
|
||||||
|
|
||||||
|
const users = ref<UserData[]>([])
|
||||||
|
const customers = ref<CustomerData[]>([])
|
||||||
|
const trucks = ref<TruckData[]>([])
|
||||||
|
const carriers = ref<CarrierData[]>([])
|
||||||
|
const drivers = ref<DriverData[]>([])
|
||||||
|
const vehicles = ref<VehicleData[]>([])
|
||||||
|
|
||||||
|
const isLoadingUsers = ref(false)
|
||||||
|
const isLoadingShipmentTypes = ref(false)
|
||||||
|
const isLoadingCustomers = ref(false)
|
||||||
|
const isLoadingTrucks = ref(false)
|
||||||
|
const isLoadingCarriers = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
const isLoadingVehicles = ref(false)
|
||||||
|
const allowAnyLicensePlate = ref(false)
|
||||||
|
const isLoadingDrivers = ref(false)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const bovineQuantities = ref<Record<string, number | null>>({})
|
||||||
|
const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||||
|
// Transporteur sélectionné dans le formulaire
|
||||||
|
const selectedCarrier = computed(() =>
|
||||||
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
|
)
|
||||||
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
|
|
||||||
|
const form = reactive<ShipmentFormData>({
|
||||||
|
userId: '',
|
||||||
|
shipmentDate: new Date().toISOString().slice(0, 10),
|
||||||
|
customerId: '',
|
||||||
|
addressId: '',
|
||||||
|
truckId: '',
|
||||||
|
carrierId: '',
|
||||||
|
driverId: '',
|
||||||
|
vehicleId: '',
|
||||||
|
licencePlate: '',
|
||||||
|
})
|
||||||
|
// Adresses liées au client sélectionné
|
||||||
|
const customerAddresses = computed<AddressData[]>(() => {
|
||||||
|
const customerId = Number(form.customerId)
|
||||||
|
if (!Number.isFinite(customerId)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return customers.value.find((customer) => customer.id === customerId)?.addresses ?? []
|
||||||
|
})
|
||||||
|
// Options pour le select des adresses du client
|
||||||
|
const customerAddressOptions = computed(() =>
|
||||||
|
customerAddresses.value
|
||||||
|
.map((address) => ({
|
||||||
|
value: String(address.id),
|
||||||
|
label: address.fullAddress
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
// Chauffeurs liés au transporteur sélectionné (LIOT)
|
||||||
|
const filteredDrivers = computed<DriverData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
|
||||||
|
})
|
||||||
|
// Véhicules liés au transporteur + camion sélectionnés (LIOT)
|
||||||
|
const filteredVehicles = computed<VehicleData[]>(() => {
|
||||||
|
if (!form.carrierId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return vehicles.value.filter(
|
||||||
|
(vehicle) =>
|
||||||
|
String(vehicle.carrier?.id) === form.carrierId &&
|
||||||
|
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// Chargement des données pour les selects
|
||||||
|
const loadUsers = async () => {
|
||||||
|
isLoadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await getUsers()
|
||||||
|
} finally {
|
||||||
|
isLoadingUsers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadShipmentType = async () => {
|
||||||
|
isLoadingShipmentTypes.value = true
|
||||||
|
try {
|
||||||
|
bovineShipment.value = await getShipmentTypeList()
|
||||||
|
} finally {
|
||||||
|
isLoadingShipmentTypes.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCustomers = async () => {
|
||||||
|
isLoadingCustomers.value = true
|
||||||
|
try {
|
||||||
|
customers.value = await getCustomerList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCustomers.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const loadTrucks = async () => {
|
||||||
|
isLoadingTrucks.value = true
|
||||||
|
try {
|
||||||
|
trucks.value = await getTruckList()
|
||||||
|
} finally {
|
||||||
|
isLoadingTrucks.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadCarriers = async () => {
|
||||||
|
isLoadingCarriers.value = true
|
||||||
|
try {
|
||||||
|
carriers.value = await getCarrierList()
|
||||||
|
} finally {
|
||||||
|
isLoadingCarriers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadVehicles = async () => {
|
||||||
|
isLoadingVehicles.value = true
|
||||||
|
try {
|
||||||
|
vehicles.value = await getVehicleList()
|
||||||
|
} finally {
|
||||||
|
isLoadingVehicles.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadDrivers = async () => {
|
||||||
|
isLoadingDrivers.value = true
|
||||||
|
try {
|
||||||
|
drivers.value = await getDriverList()
|
||||||
|
} finally {
|
||||||
|
isLoadingDrivers.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// On met le user connecté par défaut dans le select
|
||||||
|
const setDefaultUser = () => {
|
||||||
|
if (form.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (authStore.user?.id) {
|
||||||
|
form.userId = String(authStore.user.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Chargement initial des données
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadShipmentType()
|
||||||
|
await loadUsers()
|
||||||
|
await loadCustomers()
|
||||||
|
await loadTrucks()
|
||||||
|
await loadCarriers()
|
||||||
|
await loadVehicles()
|
||||||
|
await loadDrivers()
|
||||||
|
await authStore.ensureSession()
|
||||||
|
setDefaultUser()
|
||||||
|
})
|
||||||
|
// Hydrate le formulaire depuis l'expédition en cours
|
||||||
|
watch(
|
||||||
|
() => shipmentStore.current,
|
||||||
|
(shipment) => {
|
||||||
|
isHydrating.value = true
|
||||||
|
form.licencePlate = shipment?.licencePlate ?? ''
|
||||||
|
form.shipmentDate = shipment?.shipmentDate ?? new Date().toISOString().slice(0, 10)
|
||||||
|
form.userId = shipment?.user?.id ? String(shipment.user.id) :
|
||||||
|
form.userId
|
||||||
|
form.customerId = shipment?.customer?.id ?
|
||||||
|
String(shipment.customer.id) : ''
|
||||||
|
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
|
||||||
|
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
|
||||||
|
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
|
||||||
|
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
|
||||||
|
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
|
||||||
|
if (!shipment || !shipment.bovinShipments) {
|
||||||
|
bovineQuantities.value = {}
|
||||||
|
} else {
|
||||||
|
const next: Record<string, number | null> = {}
|
||||||
|
for (const entry of shipment.bovinShipments) {
|
||||||
|
const typeId = entry.shipmentType?.id
|
||||||
|
if (!typeId) continue
|
||||||
|
next[String(typeId)] = entry.nbBovinSend ?? null
|
||||||
|
}
|
||||||
|
bovineQuantities.value = next
|
||||||
|
}
|
||||||
|
isHydrating.value = false
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.customerId, form.addressId, customers.value],
|
||||||
|
() => {
|
||||||
|
if (!form.customerId) {
|
||||||
|
form.addressId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId && customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.addressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = customerAddresses.value.some(
|
||||||
|
(address) => String(address.id) === form.addressId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
if (customerAddresses.value.length === 1) {
|
||||||
|
form.addressId = String(customerAddresses.value[0].id)
|
||||||
|
} else {
|
||||||
|
form.addressId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
||||||
|
const applyLiotDefaults = () => {
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.carrierId) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
form.driverId = ''
|
||||||
|
form.vehicleId = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredDrivers.value.length === 1) {
|
||||||
|
form.driverId = String(filteredDrivers.value[0].id)
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => form.carrierId,
|
||||||
|
() => {
|
||||||
|
applyLiotDefaults()
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => isHydrating.value,
|
||||||
|
(value) => {
|
||||||
|
if (!value) {
|
||||||
|
applyLiotDefaults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.truckId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (filteredVehicles.value.length === 1) {
|
||||||
|
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = filteredVehicles.value.some(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (!matches) {
|
||||||
|
form.vehicleId = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
||||||
|
watch(
|
||||||
|
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isHydrating.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selected = filteredVehicles.value.find(
|
||||||
|
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
form.licencePlate = selected.plate
|
||||||
|
allowAnyLicensePlate.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => [form.licencePlate, form.carrierId, vehicles.value],
|
||||||
|
() => {
|
||||||
|
if (!isLiotCarrier.value || form.vehicleId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const match = filteredVehicles.value.find(
|
||||||
|
(vehicle) => vehicle.plate === form.licencePlate
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
form.vehicleId = String(match.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const buildDesiredBovinShipments = () => {
|
||||||
|
return bovineShipment.value
|
||||||
|
.map((type) => {
|
||||||
|
const raw = bovineQuantities.value[String(type.id)]
|
||||||
|
const quantity = raw === null || raw === undefined ? 0 : Number(raw)
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
quantity: Number.isFinite(quantity) ? Math.max(0, Math.trunc(quantity)) : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((entry) => entry.quantity > 0)
|
||||||
|
}
|
||||||
|
const syncBovinShipments = async (
|
||||||
|
shipmentId: number,
|
||||||
|
existing: Array<{ id?: number; nbBovinSend: number | null; shipmentType?: unknown }> = []
|
||||||
|
) => {
|
||||||
|
const shipmentIri = `/api/shipments/${shipmentId}`
|
||||||
|
const desired = buildDesiredBovinShipments()
|
||||||
|
const desiredByTypeId = new Map<number, number>()
|
||||||
|
for (const entry of desired) {
|
||||||
|
desiredByTypeId.set(entry.type.id, entry.quantity)
|
||||||
|
}
|
||||||
|
for (const entry of existing) {
|
||||||
|
if (!entry.id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const rawType = entry.shipmentType
|
||||||
|
let typeId: number | null = null
|
||||||
|
if (rawType && typeof rawType === 'object' && 'id' in rawType) {
|
||||||
|
typeId = Number((rawType as { id: number }).id)
|
||||||
|
} else if (typeof rawType === 'string') {
|
||||||
|
const match = rawType.match(/\/shipment_types\/(\\d+)$/)
|
||||||
|
typeId = match ? Number(match[1]) : null
|
||||||
|
}
|
||||||
|
if (!typeId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const desiredQuantity = desiredByTypeId.get(typeId)
|
||||||
|
if (!desiredQuantity) {
|
||||||
|
await deleteShipmentBovine(entry.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (entry.nbBovinSend !== desiredQuantity) {
|
||||||
|
await updateShipmentBovine(entry.id, {nbBovinSend: desiredQuantity})
|
||||||
|
}
|
||||||
|
desiredByTypeId.delete(typeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [typeId, quantity] of desiredByTypeId.entries()) {
|
||||||
|
await createShipmentBovine({
|
||||||
|
shipment: shipmentIri,
|
||||||
|
shipmentType: `/api/shipment_types/${typeId}`,
|
||||||
|
nbBovinSend: quantity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const buildPayload = () => {
|
||||||
|
const normalizedLicensePlate = form.licencePlate.trim()
|
||||||
|
const normalizedShipmentDate = form.shipmentDate.trim()
|
||||||
|
const normalizedCustomerId = form.customerId.trim()
|
||||||
|
const normalizedTruckId = form.truckId.trim()
|
||||||
|
const normalizedCarrierId = form.carrierId.trim()
|
||||||
|
const normalizedDriverId = form.driverId.trim()
|
||||||
|
const normalizedUserId = form.userId.trim()
|
||||||
|
const normalizedAddressId = form.addressId.trim()
|
||||||
|
const customerIri = normalizedCustomerId
|
||||||
|
? `/api/customers/${normalizedCustomerId}`
|
||||||
|
: null
|
||||||
|
const truckIri = normalizedTruckId
|
||||||
|
? `/api/trucks/${normalizedTruckId}`
|
||||||
|
: null
|
||||||
|
const carrierIri = normalizedCarrierId
|
||||||
|
? `/api/carriers/${normalizedCarrierId}`
|
||||||
|
: null
|
||||||
|
const userIri = normalizedUserId
|
||||||
|
? `/api/users/${normalizedUserId}`
|
||||||
|
: null
|
||||||
|
const driverIri = normalizedDriverId
|
||||||
|
? `/api/drivers/${normalizedDriverId}`
|
||||||
|
: null
|
||||||
|
const addressIri = normalizedAddressId
|
||||||
|
? `/api/addresses/${normalizedAddressId}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
return {
|
||||||
|
licencePlate: normalizedLicensePlate,
|
||||||
|
shipmentDate: normalizedShipmentDate,
|
||||||
|
customer: customerIri,
|
||||||
|
truck: truckIri,
|
||||||
|
carrier: carrierIri,
|
||||||
|
driver: driverIri,
|
||||||
|
user: userIri,
|
||||||
|
address: addressIri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDraft = async () => {
|
||||||
|
const payload = buildPayload()
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
const created = await shipmentStore.createShipment({
|
||||||
|
currentStep: 0,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
if (created) {
|
||||||
|
await syncBovinShipments(created.id, [])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: shipmentStore.current.currentStep,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
await syncBovinShipments(
|
||||||
|
shipmentStore.current.id,
|
||||||
|
shipmentStore.current?.bovinShipments ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({saveDraft})
|
||||||
|
// Valide le formulaire et crée/met à jour l'expédition
|
||||||
|
const validate = async () => {
|
||||||
|
const payload = buildPayload()
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
const created = await shipmentStore.createShipment({
|
||||||
|
currentStep: 1,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
if (created) {
|
||||||
|
await shipmentStore.loadShipment(created.id)
|
||||||
|
await syncBovinShipments(created.id, shipmentStore.current?.bovinShipments ?? [])
|
||||||
|
await router.push(`/shipment/${created.id}`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextStep = shipmentStore.current.currentStep + 1
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
await shipmentStore.loadShipment(shipmentStore.current.id)
|
||||||
|
await syncBovinShipments(shipmentStore.current.id, shipmentStore.current?.bovinShipments ?? [])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
101
frontend/components/shipment/shipment-weight.vue
Normal file
101
frontend/components/shipment/shipment-weight.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
|
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1>
|
||||||
|
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
|
||||||
|
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
||||||
|
<div
|
||||||
|
v-if="showLoadingBox"
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
|
<UiLoadingDots />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="displayWeight !== null" class="w-full">
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
|
||||||
|
{{ displayWeight }} kg
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-[54px]">
|
||||||
|
<button
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
@click="fetchWeight"
|
||||||
|
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
|
||||||
|
<button
|
||||||
|
v-if="displayWeight !== null && !showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="saveWeight"
|
||||||
|
>Valider la pesée</button>
|
||||||
|
<button
|
||||||
|
v-if="showGenerateReceipt"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||||
|
@click="printReceipt"
|
||||||
|
>Générer le bon</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useWeighingShipment } from '~/composables/useWeighing'
|
||||||
|
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||||
|
import { useShipmentStore } from '~/stores/shipment'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'gross' | 'tare'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const { current: storeShipment } = storeToRefs(shipmentStore)
|
||||||
|
const { printPdf } = usePdfPrinter()
|
||||||
|
const {
|
||||||
|
displayWeight,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
} = useWeighingShipment({
|
||||||
|
modeShipment: props.mode,
|
||||||
|
shipment: storeShipment,
|
||||||
|
updateShipment: shipmentStore.updateShipment,
|
||||||
|
loadShipment: shipmentStore.loadShipment
|
||||||
|
})
|
||||||
|
// Affiche le bouton de génération du bon à l'étape tare
|
||||||
|
const showGenerateReceipt = computed(
|
||||||
|
() => props.mode === 'tare' && displayWeight.value !== null
|
||||||
|
)
|
||||||
|
|
||||||
|
// Génère le bon d'expédition, puis clôture l'expédition
|
||||||
|
const printReceipt = async () => {
|
||||||
|
if (!import.meta.client || !shipmentStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWeight()
|
||||||
|
const shipment = shipmentStore.current
|
||||||
|
const filename = `${shipment.identificationNumber ?? shipment.id}_${shipment.customer?.label ?? 'client'}_${shipment.licencePlate ?? 'immat'}.pdf`
|
||||||
|
await printPdf(`/shipments/${shipment.id}/receipt`, filename)
|
||||||
|
|
||||||
|
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
const result = await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
isValid: true
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shipmentStore.clearCurrent()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère le poids dès l'arrivée sur l'écran
|
||||||
|
onMounted(() => {
|
||||||
|
if (displayWeight.value === null) {
|
||||||
|
fetchWeight()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="id"
|
:for="id"
|
||||||
class="text-xl text-bold flex items-center gap-2"
|
class="text-xl flex items-center"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="code" class="text-neutral-600">
|
v-if="code"
|
||||||
|
class="text-neutral-600">
|
||||||
({{ code }})
|
({{ code }})
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -24,7 +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-48"
|
class="border-b border-black text-xl bg-transparent w-16"
|
||||||
: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,123 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form @submit.prevent="validate">
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between gap-10">
|
|
||||||
<h1 class="text-3xl font-bold uppercase">
|
|
||||||
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
|
|
||||||
</h1>
|
|
||||||
<button
|
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{{ userId ? 'Sauvegarder' : 'Ajouter' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-y-16 gap-x-40 mb-16">
|
|
||||||
<UiTextInput
|
|
||||||
id="user-name"
|
|
||||||
v-model="form.username"
|
|
||||||
label="Nom de l'utilisateur"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UiSelect
|
|
||||||
id="user-role"
|
|
||||||
v-model="form.role"
|
|
||||||
label="Rôle de l'utilisateur"
|
|
||||||
:options="ROLE"
|
|
||||||
/>
|
|
||||||
<UiTextInput
|
|
||||||
id="user-password"
|
|
||||||
v-model="form.password"
|
|
||||||
label="Mot de passe"
|
|
||||||
type="password"
|
|
||||||
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import {computed, reactive, ref, watch} from 'vue'
|
|
||||||
import {ROLE} from '~/utils/constants'
|
|
||||||
import {createUser, updateUser, getUser} from '~/services/auth'
|
|
||||||
import type {UserData, UserFormData} from '~/services/dto/user-data'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const userId = computed(() => resolveUserId(route.params.id))
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const isHydrating = ref(false)
|
|
||||||
|
|
||||||
const resolveUserId = (param: unknown) => {
|
|
||||||
const idStr = Array.isArray(param) ? param[0] : param
|
|
||||||
if (!idStr) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const id = Number(idStr)
|
|
||||||
return Number.isFinite(id) ? id : null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const form = reactive<UserFormData>({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
role: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const hydrateFromUser = (user: UserData | null) => {
|
|
||||||
if (!user) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isHydrating.value = true
|
|
||||||
form.username = user.username ?? ''
|
|
||||||
const roles = user.roles ?? []
|
|
||||||
const hasAdmin = roles.includes("ROLE_ADMIN")
|
|
||||||
form.role = hasAdmin ? "ROLE_ADMIN" : "ROLE_USER"
|
|
||||||
form.password = ''
|
|
||||||
isHydrating.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => userId.value,
|
|
||||||
async (id) => {
|
|
||||||
if (id === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
const user = await getUser(id)
|
|
||||||
hydrateFromUser(user)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function validate() {
|
|
||||||
|
|
||||||
const normalizedUsername = form.username.trim()
|
|
||||||
const normalizedRole = form.role.trim()
|
|
||||||
const normalizedPassword = form.password.trim()
|
|
||||||
|
|
||||||
const basePayload = {
|
|
||||||
username: normalizedUsername,
|
|
||||||
roles: normalizedRole ? [normalizedRole] : undefined,
|
|
||||||
password: normalizedPassword || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId.value) {
|
|
||||||
await updateUser(userId.value, basePayload)
|
|
||||||
await router.push(`/admin/user/list/`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await createUser(basePayload)
|
|
||||||
if (created) {
|
|
||||||
await router.push(`/admin/user/list/`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,30 +1,26 @@
|
|||||||
import {useApi} from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
|
|
||||||
export const usePdfPrinter = () => {
|
export const usePdfPrinter = () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const receptionStore = useReceptionStore()
|
|
||||||
const currentReception = receptionStore.current
|
|
||||||
|
|
||||||
const printPdf = async (url: string): Promise<void> => {
|
const printPdf = async (url: string, filename = 'document.pdf'): Promise<void> => {
|
||||||
const blob = await api.getBlob(url);
|
const blob = await api.getBlob(url)
|
||||||
|
|
||||||
const pdfBlob = blob.type === 'application/pdf'
|
const pdfBlob = blob.type === 'application/pdf'
|
||||||
? blob
|
? blob
|
||||||
: new Blob([blob], { type: 'application/pdf' });
|
: new Blob([blob], { type: 'application/pdf' })
|
||||||
|
|
||||||
const blobUrl = URL.createObjectURL(pdfBlob);
|
const blobUrl = URL.createObjectURL(pdfBlob)
|
||||||
|
|
||||||
const filename = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}.pdf`;
|
const a = document.createElement('a')
|
||||||
|
a.href = blobUrl
|
||||||
const a = document.createElement('a');
|
a.download = filename
|
||||||
a.href = blobUrl;
|
a.style.display = 'none'
|
||||||
a.download = filename;
|
document.body.appendChild(a)
|
||||||
a.style.display = 'none';
|
a.click()
|
||||||
document.body.appendChild(a);
|
a.remove()
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
|
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
|
||||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,23 +3,20 @@ import {computed, ref} from 'vue'
|
|||||||
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
|
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
|
||||||
import type {WeightData} from '~/services/dto/weight-data'
|
import type {WeightData} from '~/services/dto/weight-data'
|
||||||
import {getWeight} from '~/services/reception'
|
import {getWeight} from '~/services/reception'
|
||||||
|
import {getWeightShipment} from '~/services/shipment'
|
||||||
import {createWeight, updateWeight} from '~/services/weight'
|
import {createWeight, updateWeight} from '~/services/weight'
|
||||||
|
import type {UseWeighingShipmentOptions, UseWeighingOptions} from '~/services/weight'
|
||||||
|
import type {WeightShipmentEntryData} from "~/services/dto/shipment-data";
|
||||||
|
|
||||||
export type WeighingMode = 'gross' | 'tare'
|
export type WeighingMode = 'gross' | 'tare'
|
||||||
|
|
||||||
type UseWeighingOptions = {
|
|
||||||
mode: WeighingMode
|
|
||||||
reception: Ref<ReceptionData | null>
|
|
||||||
updateReception: (id: number, payload: ReceptionPayload) => Promise<ReceptionData | null>
|
|
||||||
loadReception?: (id: number) => Promise<ReceptionData | null>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWeighing = ({
|
export const useWeighing = ({
|
||||||
mode,
|
mode,
|
||||||
reception,
|
reception,
|
||||||
updateReception,
|
updateReception,
|
||||||
loadReception
|
loadReception
|
||||||
}: UseWeighingOptions) => {
|
}: UseWeighingOptions) => {
|
||||||
const weightData = ref<WeightData | null>(null)
|
const weightData = ref<WeightData | null>(null)
|
||||||
const isFetching = ref(false)
|
const isFetching = ref(false)
|
||||||
|
|
||||||
@@ -97,3 +94,87 @@ export const useWeighing = ({
|
|||||||
saveWeight
|
saveWeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useWeighingShipment = ({
|
||||||
|
modeShipment,
|
||||||
|
shipment,
|
||||||
|
updateShipment,
|
||||||
|
loadShipment
|
||||||
|
}: UseWeighingShipmentOptions) => {
|
||||||
|
const weightData = ref<WeightData | null>(null)
|
||||||
|
const isFetching = ref(false)
|
||||||
|
|
||||||
|
const currentWeightEntry = computed<WeightShipmentEntryData | null>(() => {
|
||||||
|
const weights = shipment.value?.weights ?? []
|
||||||
|
return weights.find((entry) => entry.type === modeShipment) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
|
||||||
|
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
|
||||||
|
const title = computed(() => (modeShipment === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
|
||||||
|
const showLoadingBox = computed(
|
||||||
|
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchWeight = async () => {
|
||||||
|
isFetching.value = true
|
||||||
|
weightData.value = await getWeightShipment().finally(() => {
|
||||||
|
isFetching.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveWeight = async () => {
|
||||||
|
if (!shipment.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingEntry = currentWeightEntry.value
|
||||||
|
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
|
||||||
|
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
|
||||||
|
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
|
||||||
|
|
||||||
|
if (baseWeight === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEntry?.id) {
|
||||||
|
await updateWeight(existingEntry.id, {
|
||||||
|
type: modeShipment,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createWeight({
|
||||||
|
shipment: `api/shipments/${shipment.value.id}`,
|
||||||
|
type: modeShipment,
|
||||||
|
dsd: baseDsd,
|
||||||
|
weight: baseWeight,
|
||||||
|
weighedAt: baseWeighedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = modeShipment === 'tare'
|
||||||
|
? shipment.value.currentStep
|
||||||
|
: shipment.value.currentStep + 1
|
||||||
|
await updateShipment(shipment.value.id, {
|
||||||
|
currentStep: nextStep,
|
||||||
|
isValid: shipment.value.isValid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loadShipment) {
|
||||||
|
await loadShipment(shipment.value.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weightData,
|
||||||
|
currentWeightEntry,
|
||||||
|
displayWeight,
|
||||||
|
displayDsd,
|
||||||
|
title,
|
||||||
|
showLoadingBox,
|
||||||
|
fetchWeight,
|
||||||
|
saveWeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ 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éceptionnées',
|
||||||
TareWeighing = 'Pesée à vide'
|
TareWeighing = 'Pesée à vide',
|
||||||
|
Shipment = 'Expédition',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RECEPTION_STEP_LABELS = [
|
export const RECEPTION_STEP_LABELS = [
|
||||||
@@ -11,3 +12,9 @@ export const RECEPTION_STEP_LABELS = [
|
|||||||
StepLabel.Selection,
|
StepLabel.Selection,
|
||||||
StepLabel.TareWeighing
|
StepLabel.TareWeighing
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const SHIPMENT_STEP_LABELS = [
|
||||||
|
StepLabel.Shipment,
|
||||||
|
StepLabel.TareWeighing,
|
||||||
|
StepLabel.GrossWeighing,
|
||||||
|
]
|
||||||
|
|||||||
@@ -17,6 +17,22 @@
|
|||||||
"weight": {
|
"weight": {
|
||||||
"update": "Impossible de mettre à jour la pesée"
|
"update": "Impossible de mettre à jour la pesée"
|
||||||
},
|
},
|
||||||
|
"shipment": {
|
||||||
|
"list": "Impossible de récupérer la liste des éxpeditions.",
|
||||||
|
"fetch": "Impossible de récupérer l'éxpeditions.",
|
||||||
|
"create": "Impossible de créer l'éxpeditions.",
|
||||||
|
"update": "Impossible de mettre à jour l'éxpeditions.",
|
||||||
|
"weigh": "Impossible de récupérer la pesée."
|
||||||
|
},
|
||||||
|
"shipmentBovine": {
|
||||||
|
"list": "Impossible de récupérer la liste des bovins de l'éxpedition.",
|
||||||
|
"create": "Impossible d'enregistrer le bovin.",
|
||||||
|
"delete": "Impossible de supprimer le bovin.",
|
||||||
|
"update": "Impossible de mettre à jour le bovin."
|
||||||
|
},
|
||||||
|
"shipmentType": {
|
||||||
|
"list": "Impossible de récupérer la liste des types d'éxpedition."
|
||||||
|
},
|
||||||
"receptionType": {
|
"receptionType": {
|
||||||
"list": "Impossible de récupérer la liste des types de réception."
|
"list": "Impossible de récupérer la liste des types de réception."
|
||||||
},
|
},
|
||||||
@@ -40,7 +56,27 @@
|
|||||||
"delete": "Impossible de supprimer le bovin."
|
"delete": "Impossible de supprimer le bovin."
|
||||||
},
|
},
|
||||||
"supplier": {
|
"supplier": {
|
||||||
"list": "Impossible de récupérer la liste des fournisseurs."
|
"list": "Impossible de récupérer la liste des fournisseurs.",
|
||||||
|
"fetch": "Impossible de récupérer le fournisseur.",
|
||||||
|
"create": "Impossible de créer le fournisseur.",
|
||||||
|
"update": "Impossible de mettre à jour le fournisseur.",
|
||||||
|
"nameRequired": "Le nom du fournisseur est obligatoire."
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"fetch": "Impossible de récupérer l'adresse.",
|
||||||
|
"create": "Impossible de créer l'adresse.",
|
||||||
|
"update": "Impossible de mettre à jour l'adresse.",
|
||||||
|
"entityNotFound": "Entité introuvable.",
|
||||||
|
"streetRequired": "La rue est obligatoire.",
|
||||||
|
"postalCodeRequired": "Le code postal est obligatoire.",
|
||||||
|
"cityRequired": "La ville est obligatoire.",
|
||||||
|
"countryCodeInvalid": "Le pays doit être un code ISO2 (2 lettres)."
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"list": "Impossible de récupérer la liste des clients.",
|
||||||
|
"fetch": "Impossible de récupérer le client.",
|
||||||
|
"create": "Impossible de créer le client.",
|
||||||
|
"update": "Impossible de mettre à jour le client."
|
||||||
},
|
},
|
||||||
"truck": {
|
"truck": {
|
||||||
"list": "Impossible de récupérer la liste des camions."
|
"list": "Impossible de récupérer la liste des camions."
|
||||||
@@ -53,7 +89,6 @@
|
|||||||
"fetch": "Impossible de récupérer les données du transporteur",
|
"fetch": "Impossible de récupérer les données du transporteur",
|
||||||
"update": "Impossible de mettre à jour le transporteur",
|
"update": "Impossible de mettre à jour le transporteur",
|
||||||
"create": "Impossible de créer le transporteur"
|
"create": "Impossible de créer le transporteur"
|
||||||
|
|
||||||
},
|
},
|
||||||
"driver": {
|
"driver": {
|
||||||
"list": "Impossible de récupérer la liste des chauffeurs."
|
"list": "Impossible de récupérer la liste des chauffeurs."
|
||||||
@@ -73,6 +108,21 @@
|
|||||||
"reception": {
|
"reception": {
|
||||||
"update": "Réception mise à jour avec succès."
|
"update": "Réception mise à jour avec succès."
|
||||||
},
|
},
|
||||||
|
"shipment": {
|
||||||
|
"update": "Éxpedition mise à jour avec succès."
|
||||||
|
},
|
||||||
|
"supplier": {
|
||||||
|
"create": "Fournisseur créé avec succès.",
|
||||||
|
"update": "Fournisseur mis à jour avec succès."
|
||||||
|
},
|
||||||
|
"customer": {
|
||||||
|
"create": "Client créé avec succès.",
|
||||||
|
"update": "Client mis à jour avec succès."
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"create": "Adresse créée avec succès.",
|
||||||
|
"update": "Adresse mise à jour avec succès."
|
||||||
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"update": "Utilisateur mis à jour avec succès.",
|
"update": "Utilisateur mis à jour avec succès.",
|
||||||
"create": "Utilisateur créé avec succès.",
|
"create": "Utilisateur créé avec succès.",
|
||||||
|
|||||||
@@ -24,34 +24,64 @@
|
|||||||
<aside class="bg-primary-500 text-white min-h-0 flex flex-col justify-between">
|
<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">
|
<div class="flex flex-col gap-4 p-4 font-bold text-xl">
|
||||||
<!-- Liste des liens à ajouter ci-dessous -->
|
<!-- Liste des liens à ajouter ci-dessous -->
|
||||||
<NuxtLink to="/admin/dashboard">
|
<NuxtLink
|
||||||
Tableau de bord
|
to="/admin/dashboard"
|
||||||
|
custom v-slot="{ href, navigate, isExactActive }">
|
||||||
|
<a :href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="isExactActive ? 'opacity-100' : 'opacity-50'">
|
||||||
|
Tableau de bord
|
||||||
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/admin/supplier/supplier-list">
|
<NuxtLink
|
||||||
Fournisseur
|
to="/admin/supplier/supplier-list"
|
||||||
|
custom v-slot="{ href, navigate }">
|
||||||
|
<a :href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/supplier') ? 'opacity-100' : 'opacity-50'">
|
||||||
|
Fournisseur
|
||||||
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/admin/carrier/carrier-list">
|
<NuxtLink
|
||||||
Transporteur
|
to="/admin/carrier/carrier-list"
|
||||||
|
custom v-slot="{ href, navigate }">
|
||||||
|
<a :href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/carrier') ? 'opacity-100' : 'opacity-50'">
|
||||||
|
Transporteur
|
||||||
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/admin/user/list">
|
<NuxtLink to="/admin/user/list" custom v-slot="{ href, navigate }">
|
||||||
Utilisateurs
|
<a
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
:class="route.path.startsWith('/admin/user') ? 'opacity-100' : 'opacity-50'"
|
||||||
|
>
|
||||||
|
Utilisateurs
|
||||||
|
</a>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/admin/customer/customer-list">
|
||||||
|
Client
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<p class="font-bold text-white text-left">v{{ version }}</p>
|
|
||||||
<button
|
<button
|
||||||
@click="handleLogout"
|
@click="handleLogout"
|
||||||
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
|
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
|
||||||
>
|
>
|
||||||
Déconnexion
|
Déconnexion
|
||||||
</button>
|
</button>
|
||||||
|
<p class="font-bold text-white text-center pt-2">
|
||||||
|
v{{ version }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="min-h-0 overflow-auto px-12 py-12 ">
|
<main class="min-h-0 overflow-auto px-12 py-12 ">
|
||||||
<div class="w-full ">
|
<div class="w-full ">
|
||||||
<slot />
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +93,9 @@
|
|||||||
import {useAuthStore} from '~/stores/auth'
|
import {useAuthStore} from '~/stores/auth'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const { version } = useAppVersion()
|
const {version} = useAppVersion()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-white text-neutral-900">
|
<div class="min-h-screen text-neutral-900 grid grid-rows-[85px,1fr]">
|
||||||
<header class="w-full border-b border-neutral-200 bg-primary-500">
|
<header class="w-full border-b border-neutral-200 bg-primary-500">
|
||||||
<div class="flex w-full items-center justify-center px-6 py-4">
|
<div class="flex w-full items-center justify-center px-6 py-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -21,12 +21,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/dashboard" custom v-slot="{ href, navigate, isActive }"
|
to="/admin/dashboard" custom v-slot="{ href, navigate, isExactActive }"
|
||||||
v-if="auth.isAdmin"
|
v-if="auth.isAdmin"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
|
:class="isExactActive ? 'opacity-100' : 'opacity-50'"
|
||||||
>
|
>
|
||||||
Admin
|
Admin
|
||||||
</a>
|
</a>
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
</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]">
|
||||||
<slot/>
|
<slot/>
|
||||||
</main>
|
</main>
|
||||||
<footer class="w-full mt-8 bg-primary-500 p-6">
|
<footer class="w-full mt-8 bg-primary-500 p-6">
|
||||||
|
|||||||
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>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
{{ customerId ? "Sauvegarder" : "Ajouter" }}
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="customerId === null || !auth.isAdmin"
|
||||||
|
@click="goToAddAddress"
|
||||||
|
>
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</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: "admin"})
|
||||||
|
|
||||||
|
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: "admin" })
|
||||||
|
|
||||||
|
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>
|
||||||
116
frontend/pages/admin/customer/customer-list.vue
Normal file
116
frontend/pages/admin/customer/customer-list.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h1 class="text-3xl font-bold uppercase">Liste des Clients</h1>
|
||||||
|
<NuxtLink
|
||||||
|
to="/admin/customer"
|
||||||
|
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
|
>
|
||||||
|
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: "admin" })
|
||||||
|
|
||||||
|
const customerList = ref<CustomerData[]>([])
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const goToCustomer = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
router.push(`/admin/customer/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
customerList.value = await getCustomerList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AdminUserForm/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'admin'
|
||||||
|
|||||||
197
frontend/pages/admin/supplier/[[id]].vue
Normal file
197
frontend/pages/admin/supplier/[[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">
|
||||||
|
{{ supplierId ? "Modifications du fournisseur" : "Ajout d'un fournisseur" }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading || !auth.isAdmin"
|
||||||
|
>
|
||||||
|
{{ supplierId ? "Sauvegarder" : "Ajouter" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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-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"/>
|
||||||
|
</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 fournisseur</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="supplierId === null || !auth.isAdmin"
|
||||||
|
@click="goToAddAddress"
|
||||||
|
>
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</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 {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
|
||||||
|
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
|
||||||
|
import {useAuthStore} from "~/stores/auth"
|
||||||
|
|
||||||
|
definePageMeta({layout: "admin"})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const resolveId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) return null
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
const supplierId = computed(() => resolveId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const form = reactive<SupplierFormData>({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
addresses: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToAddAddress = () => {
|
||||||
|
if (supplierId.value === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/supplier/address",
|
||||||
|
query: {
|
||||||
|
supplierId: String(supplierId.value),
|
||||||
|
fromSupplier: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToEditAddress = (addressId: number | null) => {
|
||||||
|
if (supplierId.value === null || addressId === null || !auth.isAdmin) return
|
||||||
|
router.push({
|
||||||
|
path: "/admin/supplier/address",
|
||||||
|
query: {
|
||||||
|
supplierId: String(supplierId.value),
|
||||||
|
addressId: String(addressId),
|
||||||
|
fromSupplier: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateFromSupplier = (supplier: SupplierData | null) => {
|
||||||
|
if (!supplier) return
|
||||||
|
form.name = supplier.name ?? ""
|
||||||
|
form.email = supplier.email ?? ""
|
||||||
|
form.phone = supplier.phone ?? ""
|
||||||
|
if (!Array.isArray(supplier.addresses) || supplier.addresses.length === 0) {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof supplier.addresses[0] === "string") {
|
||||||
|
form.addresses = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addresses = supplier.addresses.map((address) => ({
|
||||||
|
id: address.id ?? null,
|
||||||
|
label: address.label ?? "",
|
||||||
|
street: address.street ?? "",
|
||||||
|
street2: address.street2 ?? null,
|
||||||
|
postalCode: address.postalCode ?? "",
|
||||||
|
city: address.city ?? "",
|
||||||
|
countryCode: address.countryCode ?? "",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => supplierId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) return
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const supplier = await getSupplier(id)
|
||||||
|
hydrateFromSupplier(supplier)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (!auth.isAdmin) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = form.name.trim()
|
||||||
|
const email = (form.email ?? "").trim() || null
|
||||||
|
const phone = (form.phone ?? "").trim() || null
|
||||||
|
|
||||||
|
const supplierPayload: SupplierPayload = {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
}
|
||||||
|
let targetId: number | null = null
|
||||||
|
|
||||||
|
if (supplierId.value !== null) {
|
||||||
|
await updateSupplier(supplierId.value, supplierPayload)
|
||||||
|
targetId = supplierId.value
|
||||||
|
} else {
|
||||||
|
const created = await createSupplier(supplierPayload)
|
||||||
|
targetId = created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.push(`/admin/supplier/${targetId}`)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
47
frontend/pages/admin/supplier/address.vue
Normal file
47
frontend/pages/admin/supplier/address.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<Address type="supplier" :address="address" @validate="validate"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {AddressData, AddressPayload} from "~/services/address";
|
||||||
|
import {createAddress, getAddress, updateAddress} from "~/services/address";
|
||||||
|
import {getSupplier, updateSupplier} from "~/services/supplier";
|
||||||
|
import type {SupplierData} from "~/services/dto/supplier-data";
|
||||||
|
|
||||||
|
definePageMeta({ layout: "admin" })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const supplierId = computed(() => { return Number(route.query.supplierId) })
|
||||||
|
const supplier = ref<SupplierData|null>(null);
|
||||||
|
const addressId = computed(() => { return route.query.addressId !== undefined ? Number(route.query.addressId) : null })
|
||||||
|
const address = ref<AddressData|null>(null)
|
||||||
|
|
||||||
|
const validate = async (address: AddressPayload) => {
|
||||||
|
try {
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
await updateAddress(addressId.value, address)
|
||||||
|
} else {
|
||||||
|
await addAddress(address)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await router.push('/admin/supplier/' + supplierId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAddress = async (address: AddressPayload) => {
|
||||||
|
const response: AddressData = await createAddress(address)
|
||||||
|
const addressIRI = `/api/addresses/${response.id}`
|
||||||
|
const existingIris = (supplier.value.addresses ?? []).map((item: any) => `/api/addresses/${item.id}`)
|
||||||
|
const next = [...new Set([...existingIris, addressIRI])]
|
||||||
|
|
||||||
|
return await updateSupplier(supplierId.value, { addresses: next })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
supplier.value = await getSupplier(supplierId.value)
|
||||||
|
if (addressId.value !== null) {
|
||||||
|
address.value = await getAddress(addressId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
<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">Liste des fournisseurs</h1>
|
||||||
<NuxtLink to="/admin/supplier"
|
<NuxtLink
|
||||||
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
to="/admin/supplier"
|
||||||
|
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
|
||||||
|
@click="handleAddClick"
|
||||||
>
|
>
|
||||||
Ajouter
|
Ajouter
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 border border-slate-200 mb-16">
|
|
||||||
|
<div v-if="auth.isAdmin" class="mt-6 border border-slate-200 mb-16">
|
||||||
<div class="max-h-96 overflow-y-auto">
|
<div class="max-h-96 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
class="sticky top-0 z-10 grid grid-cols-7 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||||
>
|
>
|
||||||
<div>Nom</div>
|
<div>Nom</div>
|
||||||
<div>Mail</div>
|
<div>Mail</div>
|
||||||
@@ -18,57 +22,91 @@
|
|||||||
<div>Complément</div>
|
<div>Complément</div>
|
||||||
<div>Code Postal</div>
|
<div>Code Postal</div>
|
||||||
<div>Ville</div>
|
<div>Ville</div>
|
||||||
|
<div>Pays</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="supplierList.length === 0" class="px-4 py-6 text-slate-400">
|
||||||
|
Aucun fournisseur.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="supplier in supplierList" :key="supplier.id">
|
<div v-for="supplier in supplierList" :key="supplier.id">
|
||||||
<template v-if="supplier.addresses?.length">
|
<div
|
||||||
|
v-if="!supplier.addresses || supplier.addresses.length === 0"
|
||||||
|
class="grid grid-cols-7 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ supplier.name }}</div>
|
||||||
|
<div class="truncate">{{ supplier.email }}</div>
|
||||||
|
<div class="col-span-1">Pas d'adresse</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
<div class="uppercase truncate">{{"—"}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else-if="supplier.addresses.length > 0">
|
||||||
<div
|
<div
|
||||||
v-for="addr in supplier.addresses"
|
v-for="(address, idx) in supplier.addresses"
|
||||||
:key="addr.id"
|
:key="address.id ?? `${supplier.id}-${idx}-${address.street}-${address.postalCode}`"
|
||||||
class="grid grid-cols-6 hover:bg-slate-50 border-t gap-4 px-4 py-2"
|
class="grid grid-cols-7 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
:class="idx > 0 ? 'pl-4 border-l-4 border-l-slate-200 bg-slate-50' : ''"
|
||||||
@click="goToSupplier(supplier.id)"
|
@click="goToSupplier(supplier.id)"
|
||||||
>
|
>
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
{{ supplier.name }}
|
{{ idx === 0 ? supplier.name : "↳" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="truncate">
|
<div class="truncate">{{ idx === 0 ? supplier.email : "" }}</div>
|
||||||
{{ supplier.email }}
|
<div class="truncate">{{ address.street || "—" }}</div>
|
||||||
</div>
|
<div class="truncate">{{ address.street2 || "—" }}</div>
|
||||||
<div class="truncate">
|
<div>{{ address.postalCode || "—" }}</div>
|
||||||
{{ addr.street }}
|
<div class="uppercase truncate">{{ address.city || "—" }}</div>
|
||||||
</div>
|
<div class="uppercase truncate">{{ address.countryCode || "—" }}</div>
|
||||||
<div class="truncate">
|
</div>
|
||||||
{{ addr.street2 }}
|
</template>
|
||||||
</div>
|
|
||||||
<div>{{ addr.postalCode }}</div>
|
<template v-else>
|
||||||
<div class="uppercase truncate">
|
<div
|
||||||
{{ addr.city }}
|
class="grid grid-cols-7 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||||
|
@click="goToSupplier(supplier.id)"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ supplier.name }}</div>
|
||||||
|
<div class="truncate">{{ supplier.email }}</div>
|
||||||
|
<div class="col-span-5 text-slate-400">
|
||||||
|
Adresses non chargées
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {SupplierData} from "~/services/dto/supplier-data"
|
import { getSupplierList } from "~/services/supplier"
|
||||||
import {getSupplierList} from "~/services/supplier"
|
import type { SupplierData } from "~/services/dto/supplier-data"
|
||||||
|
import { useAuthStore } from "~/stores/auth"
|
||||||
|
|
||||||
definePageMeta({layout: "admin"})
|
definePageMeta({ layout: "admin" })
|
||||||
|
|
||||||
const supplierList = ref<SupplierData[]>([])
|
const supplierList = ref<SupplierData[]>([])
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const goToSupplier = (id: number) => {
|
const goToSupplier = (id: number) => {
|
||||||
|
if (!auth.isAdmin) return
|
||||||
router.push(`/admin/supplier/${id}`)
|
router.push(`/admin/supplier/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddClick = (event: Event) => {
|
||||||
|
if (auth.isAdmin) return
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
supplierList.value = (await getSupplierList(false)) ?? []
|
if (!auth.isAdmin) return
|
||||||
|
supplierList.value = await getSupplierList()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,127 @@
|
|||||||
<template>
|
<template>
|
||||||
<UserForm/>
|
<form @submit.prevent="validate">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-10">
|
||||||
|
<h1 class="text-3xl font-bold uppercase">
|
||||||
|
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{{ userId ? 'Sauvegarder' : 'Ajouter' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-y-16 gap-x-40 mb-16">
|
||||||
|
<UiTextInput
|
||||||
|
id="user-name"
|
||||||
|
v-model="form.username"
|
||||||
|
label="Nom de l'utilisateur"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSelect
|
||||||
|
id="user-role"
|
||||||
|
v-model="form.role"
|
||||||
|
label="Rôle de l'utilisateur"
|
||||||
|
:options="ROLE"
|
||||||
|
/>
|
||||||
|
<UiTextInput
|
||||||
|
id="user-password"
|
||||||
|
v-model="form.password"
|
||||||
|
label="Mot de passe"
|
||||||
|
type="password"
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'admin'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
import {computed, reactive, ref, watch} from 'vue'
|
||||||
|
import {ROLE} from '~/utils/constants'
|
||||||
|
import {createUser, updateUser, getUser} from '~/services/auth'
|
||||||
|
import type {UserData, UserFormData, UserPayload} from '~/services/dto/user-data'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userId = computed(() => resolveUserId(route.params.id))
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isHydrating = ref(false)
|
||||||
|
|
||||||
|
const resolveUserId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const form = reactive<UserFormData>({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
role: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hydrateFromUser = (user: UserData | null) => {
|
||||||
|
if (!user) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHydrating.value = true
|
||||||
|
form.username = user.username ?? ''
|
||||||
|
const roles = user.roles ?? []
|
||||||
|
const hasAdmin = roles.includes("ROLE_ADMIN")
|
||||||
|
form.role = hasAdmin ? "ROLE_ADMIN" : "ROLE_USER"
|
||||||
|
form.password = ''
|
||||||
|
isHydrating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => userId.value,
|
||||||
|
async (id) => {
|
||||||
|
if (id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const user = await getUser(id)
|
||||||
|
hydrateFromUser(user)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function validate() {
|
||||||
|
|
||||||
|
const normalizedUsername = form.username.trim()
|
||||||
|
const normalizedRole = form.role.trim()
|
||||||
|
const normalizedPassword = form.password.trim()
|
||||||
|
|
||||||
|
const basePayload: UserPayload = {
|
||||||
|
username: normalizedUsername,
|
||||||
|
roles: normalizedRole ? [normalizedRole] : undefined,
|
||||||
|
}
|
||||||
|
if (normalizedPassword) {
|
||||||
|
basePayload.password = normalizedPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId.value) {
|
||||||
|
await updateUser(userId.value, basePayload)
|
||||||
|
await router.push(`/admin/user/list/`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await createUser(basePayload)
|
||||||
|
if (created) {
|
||||||
|
await router.push(`/admin/user/list/`)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<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 mt-8 gap-8 mb-8 md:mb-0">
|
||||||
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
|
||||||
<card-link label="NOUVELLE EXPÉDITION" link="/" iconName="mdi:truck-fast-outline" />
|
<card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" />
|
||||||
<card-link label="PLAN DE SITE" link="/" iconName="mdi:warehouse" />
|
<card-link label="PLAN DE SITE" link="/" iconName="mdi:warehouse" />
|
||||||
<card-link label="RÉCEPTIONS EN ATTENTE" link="/reception/waiting-reception" iconName="mdi:truck-remove-outline" />
|
<card-link label="RÉCEPTIONS EN ATTENTE" link="/reception/waiting-reception" iconName="mdi:truck-remove-outline" />
|
||||||
<card-link label="EXPÉDITIONS EN ATTENTE" link="/" iconName="mdi:truck-cargo-container" />
|
<card-link label="EXPÉDITIONS EN ATTENTE" link="/shipment/waiting-shipment" iconName="mdi:truck-cargo-container" />
|
||||||
<card-link label="CASES" link="/" iconName="mdi:cube-outline" />
|
<card-link label="CASES" link="/" iconName="mdi:cube-outline" />
|
||||||
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
|
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
|
||||||
<card-link label="EXPÉDITIONS FINIES" link="/" iconName="mdi:truck-delivery-outline" />
|
<card-link label="EXPÉDITIONS FINIES" link="/shipment/finish-shipment" iconName="mdi:truck-delivery-outline" />
|
||||||
<card-link label="PASSEPORT DU BOVIN" link="/" iconName="mdi:cow" />
|
<card-link label="PASSEPORT DU BOVIN" link="/" iconName="mdi:cow" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between h-[52px] mb-[80px]">
|
<div class="flex justify-between h-[52px] mt-6 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"
|
||||||
|
|||||||
@@ -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>
|
<button
|
||||||
<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
|
||||||
|
</button>
|
||||||
</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>
|
||||||
|
|
||||||
@@ -160,7 +176,7 @@ import type {DriverData} from '~/services/dto/driver-data'
|
|||||||
import {getDriverList} from '~/services/driver'
|
import {getDriverList} from '~/services/driver'
|
||||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||||
import {getVehicleList} from '~/services/vehicle'
|
import {getVehicleList} from '~/services/vehicle'
|
||||||
import {SUPLLIER_CODE} from "~/utils/constants";
|
import {SUPPLIER_CODE} from "~/utils/constants";
|
||||||
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
|
||||||
import type {ReceptionData, ReceptionFormData} from "~/services/dto/reception-data";
|
import type {ReceptionData, ReceptionFormData} from "~/services/dto/reception-data";
|
||||||
import {getReception} from "~/services/reception";
|
import {getReception} from "~/services/reception";
|
||||||
@@ -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>({
|
||||||
@@ -213,7 +230,7 @@ const selectedCarrier = computed(() =>
|
|||||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||||
)
|
)
|
||||||
// Indique si le transporteur est LIOT
|
// Indique si le transporteur est LIOT
|
||||||
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPLLIER_CODE.LIOT)
|
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
|
||||||
// Adresses disponibles pour le fournisseur sélectionné
|
// Adresses disponibles pour le fournisseur sélectionné
|
||||||
const supplierAddresses = computed(() => {
|
const supplierAddresses = computed(() => {
|
||||||
const supplierId = Number(form.supplierId)
|
const supplierId = Number(form.supplierId)
|
||||||
@@ -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) {
|
||||||
|
|||||||
82
frontend/pages/shipment/[[id]].vue
Normal file
82
frontend/pages/shipment/[[id]].vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between h-[52px] mt-6 mb-[80px]">
|
||||||
|
<div class="flex flex-1 mr-16">
|
||||||
|
<UiStepper
|
||||||
|
:labels="SHIPMENT_STEP_LABELS"
|
||||||
|
:current-step="storeShipment?.currentStep ?? 0"
|
||||||
|
@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>
|
||||||
|
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
||||||
|
<ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
|
||||||
|
<ShipmentWeight v-if="storeShipment?.currentStep >= 2" mode="tare"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {SHIPMENT_STEP_LABELS} from "~/constants/steps";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useShipmentStore} from "~/stores/shipment";
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
const shipmentStore = useShipmentStore()
|
||||||
|
const {current: storeShipment} = storeToRefs(shipmentStore)
|
||||||
|
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const resolveShipmentId = (param: unknown) => {
|
||||||
|
const idStr = Array.isArray(param) ? param[0] : param
|
||||||
|
if (!idStr) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const id = Number(idStr)
|
||||||
|
return Number.isFinite(id) ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch (
|
||||||
|
() => route.params.id,
|
||||||
|
async (param) => {
|
||||||
|
const id = resolveShipmentId(param)
|
||||||
|
if (id === null) {
|
||||||
|
shipmentStore.clearCurrent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await shipmentStore.loadShipment(id)
|
||||||
|
},
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveAndHold = async () => {
|
||||||
|
if (shipmentFormRef.value) {
|
||||||
|
await shipmentFormRef.value.saveDraft()
|
||||||
|
}
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
|
const handleStepSelect = async (step: number) => {
|
||||||
|
if (!shipmentStore.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === shipmentStore.current.currentStep) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||||
|
currentStep: step
|
||||||
|
})
|
||||||
|
await shipmentStore.loadShipment(shipmentStore.current.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
81
frontend/pages/shipment/finish-shipment.vue
Normal file
81
frontend/pages/shipment/finish-shipment.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-start gap-10">
|
||||||
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/>
|
||||||
|
<h1 class="text-3xl font-bold uppercase">listes des expéditions finie</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ps-20 ">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
|
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Numéro</div>
|
||||||
|
<div>Date</div>
|
||||||
|
<div>Client</div>
|
||||||
|
<div>Adresse</div>
|
||||||
|
<div>Type d'expéditon</div>
|
||||||
|
<div>Poids</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="shipment in shipmentList"
|
||||||
|
:key="shipment
|
||||||
|
.id"
|
||||||
|
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToshipment(shipment.id)"
|
||||||
|
>
|
||||||
|
<div>{{ shipment.identificationNumber }}</div>
|
||||||
|
<div>{{ shipment.shipmentDate }}</div>
|
||||||
|
<div>{{ shipment.customer?.label }}</div>
|
||||||
|
<div>{{ shipment.address?.fullAddress }}</div>
|
||||||
|
<div>
|
||||||
|
<template v-if="formatBovinShipmentLines(shipment).length">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in formatBovinShipmentLines(shipment)"
|
||||||
|
:key="index"
|
||||||
|
class="leading-5"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>{{ formatWeighing(shipment, 'gross') }} | {{ formatWeighing(shipment, 'tare') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
import {getShipmentList} from "~/services/shipment";
|
||||||
|
|
||||||
|
const shipmentList = ref<ShipmentData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formatWeighing = (shipment: ShipmentData, type: 'gross' | 'tare') => {
|
||||||
|
const entry = shipment.weights?.find((weight) => weight.type === type)
|
||||||
|
if (!entry || entry.weight == null || entry.dsd == null) {
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
return `${entry.weight} kg`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatBovinShipmentLines = (shipment: ShipmentData) => {
|
||||||
|
if (!shipment.bovinShipments?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return shipment.bovinShipments.map((entry) => {
|
||||||
|
const label = typeof entry.shipmentType === 'string'
|
||||||
|
? entry.shipmentType
|
||||||
|
: entry.shipmentType?.label
|
||||||
|
return `${label ?? '—'} : ${entry.nbBovinSend ?? '—'}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToshipment = (id: number) => {
|
||||||
|
//router.push(`/shipment/update/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
shipmentList.value = await getShipmentList(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
73
frontend/pages/shipment/waiting-shipment.vue
Normal file
73
frontend/pages/shipment/waiting-shipment.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between ">
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/>
|
||||||
|
<h1 class="text-3xl font-bold uppercase">listes des expéditions en attente</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ps-20 ">
|
||||||
|
<div class="mt-6 border border-slate-200 mb-16 ">
|
||||||
|
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||||
|
<div>Client</div>
|
||||||
|
<div>Adresse</div>
|
||||||
|
<div>Type d'expéditions</div>
|
||||||
|
<div>Transporteur</div>
|
||||||
|
<div>Immatriculation</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="shipment in shipmentList"
|
||||||
|
:key="shipment.id"
|
||||||
|
class="grid grid-cols-5 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="goToShipment(shipment.id)"
|
||||||
|
@keydown.enter="goToShipment(shipment.id)"
|
||||||
|
>
|
||||||
|
<div>{{ shipment.customer?.label }}</div>
|
||||||
|
<div>{{ shipment.address?.fullAddress }}</div>
|
||||||
|
<div>
|
||||||
|
<template v-if="formatBovinShipmentLines(shipment).length">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in formatBovinShipmentLines(shipment)"
|
||||||
|
:key="index"
|
||||||
|
class="leading-5"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>{{ shipment.carrier?.name }}</div>
|
||||||
|
<div>{{ shipment.licencePlate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {ShipmentData} from "~/services/dto/shipment-data";
|
||||||
|
import {getShipmentList} from "~/services/shipment";
|
||||||
|
|
||||||
|
const shipmentList = ref<ShipmentData[]>()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goToShipment = (id: number) => {
|
||||||
|
router.push(`/shipment/${id}`)
|
||||||
|
}
|
||||||
|
const formatBovinShipmentLines = (shipment: ShipmentData) => {
|
||||||
|
if (!shipment.bovinShipments?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return shipment.bovinShipments.map((entry) => {
|
||||||
|
const label = typeof entry.shipmentType === 'string'
|
||||||
|
? entry.shipmentType
|
||||||
|
: entry.shipmentType?.label
|
||||||
|
return `${label ?? '—'} : ${entry.nbBovinSend ?? '—'}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
shipmentList.value = await getShipmentList(false)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
45
frontend/services/address.ts
Normal file
45
frontend/services/address.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type { AddressData } from '~/services/dto/address-data'
|
||||||
|
export interface AddressPayload {
|
||||||
|
label: string
|
||||||
|
street: string
|
||||||
|
street2?: string | null
|
||||||
|
postalCode: string
|
||||||
|
city: string
|
||||||
|
countryCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressData extends AddressPayload {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAddress(
|
||||||
|
payload: AddressPayload
|
||||||
|
): Promise<AddressData> {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return await api.post<AddressData>('addresses', payload, {
|
||||||
|
toastErrorKey: 'errors.address.create',
|
||||||
|
toastSuccessKey: 'success.address.create',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAddress(
|
||||||
|
id: number,
|
||||||
|
payload: AddressPayload
|
||||||
|
): Promise<AddressData> {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return await api.patch<AddressData>(`addresses/${id}`, payload, {
|
||||||
|
toastErrorKey: 'errors.address.update',
|
||||||
|
toastSuccessKey: 'success.address.update',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAddress(id: number): Promise<AddressData> {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return await api.get<AddressData>(`addresses/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.address.fetch',
|
||||||
|
})
|
||||||
|
}
|
||||||
50
frontend/services/bovin-shipment.ts
Normal file
50
frontend/services/bovin-shipment.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type { BovinShipmentData } from '~/services/dto/bovin-shipment-data'
|
||||||
|
import type { ShipmentBovinePayload, BovinShipmentListResponse } from '~/services/dto/bovin-shipment-data'
|
||||||
|
|
||||||
|
export async function getBovinShipmentList(
|
||||||
|
shipmentIri: string
|
||||||
|
): Promise<BovinShipmentData[]> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<BovinShipmentListResponse>(
|
||||||
|
'bovin_shipments',
|
||||||
|
{ shipment: shipmentIri },
|
||||||
|
{
|
||||||
|
toastErrorKey: 'errors.shipmentBovine.list'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||||
|
return response['hydra:member']
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createShipmentBovine(
|
||||||
|
payload: ShipmentBovinePayload
|
||||||
|
): Promise<BovinShipmentData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<BovinShipmentData>('bovin_shipments', payload, {
|
||||||
|
toastErrorKey: 'errors.shipmentBovine.create'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteShipmentBovine(id: number): Promise<void> {
|
||||||
|
const api = useApi()
|
||||||
|
await api.delete<void>(`bovin_shipments/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.shipmentBovine.delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateShipmentBovine(
|
||||||
|
id: number,
|
||||||
|
payload: Partial<ShipmentBovinePayload>
|
||||||
|
): Promise<BovinShipmentData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<BovinShipmentData>(`bovin_shipments/${id}`, payload, {
|
||||||
|
toastErrorKey: 'errors.shipmentBovine.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
43
frontend/services/customer.ts
Normal file
43
frontend/services/customer.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useApi } from "~/composables/useApi"
|
||||||
|
import type { CustomerData, CustomerPayload } from "~/services/dto/customer-data"
|
||||||
|
|
||||||
|
export type CustomerListResponse =
|
||||||
|
| CustomerData[]
|
||||||
|
| { "hydra:member"?: CustomerData[] }
|
||||||
|
|
||||||
|
export async function getCustomerList(): Promise<CustomerData[]> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<CustomerListResponse>("customers", {}, {
|
||||||
|
toastErrorKey: "errors.customer.list",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(response)) return response
|
||||||
|
if (response && typeof response === "object" && Array.isArray(response["hydra:member"])) {
|
||||||
|
return response["hydra:member"]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCustomer(id: number): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<CustomerData>(`customers/${id}`, {}, {
|
||||||
|
toastErrorKey: "errors.customer.fetch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCustomer(id: number, payload: Partial<CustomerPayload>): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<CustomerData>(`customers/${id}`, payload, {
|
||||||
|
toastErrorKey: "errors.customer.update",
|
||||||
|
toastSuccessKey: "success.customer.update",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCustomer(payload: CustomerPayload): Promise<CustomerData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<CustomerData>("customers", payload, {
|
||||||
|
toastErrorKey: "errors.customer.create",
|
||||||
|
toastSuccessKey: "success.customer.create",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,5 +6,14 @@ export interface AddressData {
|
|||||||
postalCode: string
|
postalCode: string
|
||||||
city: string
|
city: string
|
||||||
countryCode: string
|
countryCode: string
|
||||||
fullAddress?: string
|
}
|
||||||
|
|
||||||
|
export interface AddressFormData {
|
||||||
|
id?: number | null
|
||||||
|
label: string
|
||||||
|
street: string
|
||||||
|
street2?: string | null
|
||||||
|
postalCode: string
|
||||||
|
city: string
|
||||||
|
countryCode: string
|
||||||
}
|
}
|
||||||
|
|||||||
18
frontend/services/dto/bovin-shipment-data.ts
Normal file
18
frontend/services/dto/bovin-shipment-data.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
|
||||||
|
|
||||||
|
export interface BovinShipmentData {
|
||||||
|
id: number
|
||||||
|
nbBovinSend: number | null
|
||||||
|
shipment?: string | null
|
||||||
|
shipmentType?: ShipmentTypeData | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShipmentBovinePayload = {
|
||||||
|
nbBovinSend: number
|
||||||
|
shipment: string
|
||||||
|
shipmentType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BovinShipmentListResponse =
|
||||||
|
| BovinShipmentData[]
|
||||||
|
| { 'hydra:member'?: BovinShipmentData[] }
|
||||||
25
frontend/services/dto/customer-data.ts
Normal file
25
frontend/services/dto/customer-data.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
|
||||||
|
export type CustomerAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
|
export interface CustomerData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
addresses: CustomerAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerFormData {
|
||||||
|
name: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
addresses: AddressFormData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomerPayload = {
|
||||||
|
name: string
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
addresses?: string[]
|
||||||
|
}
|
||||||
67
frontend/services/dto/shipment-data.ts
Normal file
67
frontend/services/dto/shipment-data.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type {CarrierData} from '~/services/dto/carrier-data'
|
||||||
|
import type {TruckData} from '~/services/dto/truck-data'
|
||||||
|
import type {CustomerData} from '~/services/dto/customer-data'
|
||||||
|
import type {AddressData} from "~/services/dto/address-data";
|
||||||
|
|
||||||
|
export interface ShipmentTypeData {
|
||||||
|
id: number
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BovinShipmentData {
|
||||||
|
id?: number
|
||||||
|
shipmentType?: ShipmentTypeData | string | null
|
||||||
|
nbBovinSend: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShipmentData = {
|
||||||
|
id: number
|
||||||
|
identificationNumber?: string | null
|
||||||
|
licencePlate: string | null
|
||||||
|
shipmentDate: string
|
||||||
|
currentStep: number
|
||||||
|
isValid: boolean
|
||||||
|
address?: AddressData | null
|
||||||
|
carrier?: CarrierData | null
|
||||||
|
truck?: TruckData | null
|
||||||
|
customer?: CustomerData | null
|
||||||
|
bovinShipments?: BovinShipmentData[] | null
|
||||||
|
weights?: WeightShipmentEntryData[] | null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeightShipmentEntryData {
|
||||||
|
id?: number
|
||||||
|
type: 'gross' | 'tare'
|
||||||
|
dsd: number | null
|
||||||
|
weight: number | null
|
||||||
|
weighedAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShipmentFormData = {
|
||||||
|
userId: string,
|
||||||
|
shipmentDate: string,
|
||||||
|
customerId: string,
|
||||||
|
addressId: string,
|
||||||
|
truckId: string,
|
||||||
|
carrierId: string,
|
||||||
|
driverId: string,
|
||||||
|
vehicleId: string,
|
||||||
|
licencePlate: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShipmentPayload = {
|
||||||
|
licencePlate?: string | null
|
||||||
|
shipmentDate?: string
|
||||||
|
currentStep?: number
|
||||||
|
isValid?: boolean
|
||||||
|
carrier?: string | null
|
||||||
|
truck?: string | null
|
||||||
|
customer?: string | null
|
||||||
|
bovinShipments?: string[] | null
|
||||||
|
address?: string | null
|
||||||
|
user?: string | null
|
||||||
|
driver?: string | null
|
||||||
|
|
||||||
|
}
|
||||||
5
frontend/services/dto/shipment-type-data.ts
Normal file
5
frontend/services/dto/shipment-type-data.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ShipmentTypeData {
|
||||||
|
id: number
|
||||||
|
label: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
@@ -1,9 +1,25 @@
|
|||||||
import type { AddressData } from '~/services/dto/address-data'
|
import type { AddressFormData } from "~/services/dto/address-data"
|
||||||
|
|
||||||
|
export type SupplierAddresses = AddressFormData[] | string[]
|
||||||
|
|
||||||
export interface SupplierData {
|
export interface SupplierData {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
email?: string | null
|
email?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
addresses?: AddressData[] | null
|
addresses: SupplierAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupplierFormData {
|
||||||
|
name: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
addresses: AddressFormData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SupplierPayload = {
|
||||||
|
name: string
|
||||||
|
email?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
addresses?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
24
frontend/services/shipment-type.ts
Normal file
24
frontend/services/shipment-type.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
|
||||||
|
|
||||||
|
export type ShipmentTypeListResponse =
|
||||||
|
| ShipmentTypeData[]
|
||||||
|
| { 'hydra:member'?: ShipmentTypeData[] }
|
||||||
|
|
||||||
|
|
||||||
|
export async function getShipmentTypeList(): Promise<ShipmentTypeData[]> {
|
||||||
|
const api = useApi()
|
||||||
|
const response = await api.get<ShipmentTypeListResponse>('shipment_types', {}, {
|
||||||
|
toastErrorKey: 'errors.shipmentType.list'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||||
|
return response['hydra:member']
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
40
frontend/services/shipment.ts
Normal file
40
frontend/services/shipment.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {useApi} from '~/composables/useApi'
|
||||||
|
import type {ShipmentData, ShipmentPayload} from '~/services/dto/shipment-data'
|
||||||
|
import type {WeightData} from '~/services/dto/weight-data'
|
||||||
|
|
||||||
|
export async function getShipmentList(isValid: boolean|null = null) {
|
||||||
|
const api = useApi()
|
||||||
|
const query = isValid !== null ? { isValid: isValid} : {}
|
||||||
|
return api.get<ShipmentData[]>('shipments', query, {
|
||||||
|
toastErrorKey: 'errors.shipment.list'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getShipment(id: number) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<ShipmentData>(`shipments/${id}`, {}, {
|
||||||
|
toastErrorKey: 'errors.shipment.fetch'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createShipment(payload: ShipmentPayload = {}) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<ShipmentData>('shipments', payload, {
|
||||||
|
toastErrorKey: 'errors.shipment.create'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateShipment(id: number, payload: ShipmentPayload) {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<ShipmentData>(`shipments/${id}`, payload, {
|
||||||
|
toastErrorKey: 'errors.shipment.update',
|
||||||
|
toastSuccessKey: 'success.shipment.update'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWeightShipment(): Promise<WeightData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<WeightData>('shipments/weigh', {}, {
|
||||||
|
toastErrorKey: 'errors.shipment.weigh'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from "~/composables/useApi"
|
||||||
import type { SupplierData } from '~/services/dto/supplier-data'
|
import type { SupplierData, SupplierPayload } from "~/services/dto/supplier-data"
|
||||||
|
|
||||||
export type SupplierListResponse =
|
export type SupplierListResponse =
|
||||||
| SupplierData[]
|
| SupplierData[]
|
||||||
| { 'hydra:member'?: SupplierData[] }
|
| { "hydra:member"?: SupplierData[] }
|
||||||
|
|
||||||
export async function getSupplierList(): Promise<SupplierData[]> {
|
export async function getSupplierList(): Promise<SupplierData[]> {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const response = await api.get<SupplierListResponse>('suppliers', {}, {
|
const response = await api.get<SupplierListResponse>("suppliers", {}, {
|
||||||
toastErrorKey: 'errors.supplier.list'
|
toastErrorKey: "errors.supplier.list",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(response)) {
|
if (Array.isArray(response)) return response
|
||||||
return response
|
if (response && typeof response === "object" && Array.isArray(response["hydra:member"])) {
|
||||||
|
return response["hydra:member"]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
|
||||||
return response['hydra:member']
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSupplier(id: number): Promise<SupplierData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.get<SupplierData>(`suppliers/${id}`, {}, {
|
||||||
|
toastErrorKey: "errors.supplier.fetch",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSupplier(id: number, payload: Partial<SupplierPayload>): Promise<SupplierData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.patch<SupplierData>(`suppliers/${id}`, payload, {
|
||||||
|
toastErrorKey: "errors.supplier.update",
|
||||||
|
toastSuccessKey: "success.supplier.update",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSupplier(payload: SupplierPayload): Promise<SupplierData> {
|
||||||
|
const api = useApi()
|
||||||
|
return api.post<SupplierData>("suppliers", payload, {
|
||||||
|
toastErrorKey: "errors.supplier.create",
|
||||||
|
toastSuccessKey: "success.supplier.create",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import type { WeightEntryData } from '~/services/dto/reception-data'
|
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
|
||||||
|
import type {Ref} from "vue";
|
||||||
|
import type {ShipmentData, ShipmentPayload} from "~/services/dto/shipment-data";
|
||||||
|
import type {WeighingMode} from "~/composables/useWeighing";
|
||||||
|
|
||||||
export type WeightPayload = {
|
export type WeightPayload = {
|
||||||
reception: string
|
reception?: string
|
||||||
|
shipment?: string
|
||||||
type: 'gross' | 'tare'
|
type: 'gross' | 'tare'
|
||||||
dsd: number | null
|
dsd: number | null
|
||||||
weight: number | null
|
weight: number | null
|
||||||
@@ -21,3 +25,17 @@ export async function updateWeight(id: number, payload: Partial<WeightPayload>)
|
|||||||
toastSuccessKey: 'success.weight.update'
|
toastSuccessKey: 'success.weight.update'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UseWeighingShipmentOptions = {
|
||||||
|
modeShipment: WeighingMode
|
||||||
|
shipment: Ref<ShipmentData | null>
|
||||||
|
updateShipment: (id: number, payload: ShipmentPayload) => Promise<ShipmentData | null>
|
||||||
|
loadShipment?: (id: number) => Promise<ShipmentData | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseWeighingOptions = {
|
||||||
|
mode: WeighingMode
|
||||||
|
reception: Ref<ReceptionData | null>
|
||||||
|
updateReception: (id: number, payload: ReceptionPayload) => Promise<ReceptionData | null>
|
||||||
|
loadReception?: (id: number) => Promise<ReceptionData | null>
|
||||||
|
}
|
||||||
|
|||||||
58
frontend/stores/shipment.ts
Normal file
58
frontend/stores/shipment.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type {ShipmentData, ShipmentPayload} from "~/services/dto/shipment-data";
|
||||||
|
import {createShipment, getShipment, updateShipment} from "~/services/shipment";
|
||||||
|
|
||||||
|
const isShipmentData = (value: unknown): value is ShipmentData => {
|
||||||
|
return Boolean(value && typeof value === 'object' && 'id' in value)
|
||||||
|
}
|
||||||
|
export const useShipmentStore = defineStore('shipment', {
|
||||||
|
state: () => ({
|
||||||
|
current: null as ShipmentData | null,
|
||||||
|
isLoading: false
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setCurrent(shipment: ShipmentData | null) {
|
||||||
|
this.current = shipment
|
||||||
|
},
|
||||||
|
clearCurrent() {
|
||||||
|
this.current = null
|
||||||
|
},
|
||||||
|
async loadShipment(id: number) {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = await getShipment(id).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isShipmentData(result)) {
|
||||||
|
this.current = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
async createShipment(payload: ShipmentPayload = {}) {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = await createShipment(payload).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isShipmentData(result)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
async updateShipment(id: number, payload: ShipmentPayload) {
|
||||||
|
this.isLoading = true
|
||||||
|
const result = await updateShipment(id, payload).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
if (!isShipmentData(result)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -12,6 +12,6 @@ export const ROLE = [
|
|||||||
{ label: 'Administrateur', value: 'ROLE_ADMIN' },
|
{ label: 'Administrateur', value: 'ROLE_ADMIN' },
|
||||||
{ label: 'Utilisateur', value: 'ROLE_USER' }
|
{ label: 'Utilisateur', value: 'ROLE_USER' }
|
||||||
]
|
]
|
||||||
export const SUPLLIER_CODE = {
|
export const SUPPLIER_CODE = {
|
||||||
LIOT: 'LIOT'
|
LIOT: 'LIOT'
|
||||||
}
|
}
|
||||||
|
|||||||
2
makefile
2
makefile
@@ -79,7 +79,7 @@ migration-migrate:
|
|||||||
$(SYMFONY_CONSOLE) --no-interaction doctrine:migrations:migrate --allow-no-migration
|
$(SYMFONY_CONSOLE) --no-interaction doctrine:migrations:migrate --allow-no-migration
|
||||||
|
|
||||||
fixtures:
|
fixtures:
|
||||||
$(SYMFONY_CONSOLE) doctrine:fixtures:load
|
$(SYMFONY_CONSOLE) --no-interaction doctrine:fixtures:load
|
||||||
|
|
||||||
# Attention, supprime votre bdd local
|
# Attention, supprime votre bdd local
|
||||||
db-reset:
|
db-reset:
|
||||||
|
|||||||
64
migrations/Version20260204101625.php
Normal file
64
migrations/Version20260204101625.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260204101625 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE bovin_shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, nb_bovin_send INT NOT NULL, shipment_id INT DEFAULT NULL, shipment_type_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_7049F4507BE036FC ON bovin_shipment (shipment_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_7049F4502EE48A36 ON bovin_shipment (shipment_type_id)');
|
||||||
|
$this->addSql('CREATE TABLE customer (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE TABLE customer_address (customer_id INT NOT NULL, address_id INT NOT NULL, PRIMARY KEY (customer_id, address_id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_1193CB3F9395C3F3 ON customer_address (customer_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_1193CB3FF5B7AF75 ON customer_address (address_id)');
|
||||||
|
$this->addSql('CREATE TABLE shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, licence_plate VARCHAR(255) NOT NULL, identification_number VARCHAR(20) DEFAULT NULL, current_step INT DEFAULT 0 NOT NULL, is_valid BOOLEAN NOT NULL, shipment_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, carrier_id INT DEFAULT NULL, vehicle_id INT DEFAULT NULL, truck_id INT DEFAULT NULL, customer_id INT DEFAULT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_2CB20DC347639A5 ON shipment (identification_number)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DC21DFC797 ON shipment (carrier_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DC545317D1 ON shipment (vehicle_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCC6957CCE ON shipment (truck_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DC9395C3F3 ON shipment (customer_id)');
|
||||||
|
$this->addSql('CREATE TABLE shipment_type (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT FK_7049F4507BE036FC FOREIGN KEY (shipment_id) REFERENCES shipment (id)');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT FK_7049F4502EE48A36 FOREIGN KEY (shipment_type_id) REFERENCES shipment_type (id)');
|
||||||
|
$this->addSql('ALTER TABLE customer_address ADD CONSTRAINT FK_1193CB3F9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) ON DELETE CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE customer_address ADD CONSTRAINT FK_1193CB3FF5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id) ON DELETE CASCADE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC21DFC797 FOREIGN KEY (carrier_id) REFERENCES carrier (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC545317D1 FOREIGN KEY (vehicle_id) REFERENCES vehicle (id)');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCC6957CCE FOREIGN KEY (truck_id) REFERENCES truck (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT FK_7049F4507BE036FC');
|
||||||
|
$this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT FK_7049F4502EE48A36');
|
||||||
|
$this->addSql('ALTER TABLE customer_address DROP CONSTRAINT FK_1193CB3F9395C3F3');
|
||||||
|
$this->addSql('ALTER TABLE customer_address DROP CONSTRAINT FK_1193CB3FF5B7AF75');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC21DFC797');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC545317D1');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCC6957CCE');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC9395C3F3');
|
||||||
|
$this->addSql('DROP TABLE bovin_shipment');
|
||||||
|
$this->addSql('DROP TABLE customer');
|
||||||
|
$this->addSql('DROP TABLE customer_address');
|
||||||
|
$this->addSql('DROP TABLE shipment');
|
||||||
|
$this->addSql('DROP TABLE shipment_type');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
migrations/Version20260204102423.php
Normal file
35
migrations/Version20260204102423.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260204102423 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT fk_2cb20dc545317d1');
|
||||||
|
$this->addSql('DROP INDEX idx_2cb20dc545317d1');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP vehicle_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD vehicle_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT fk_2cb20dc545317d1 FOREIGN KEY (vehicle_id) REFERENCES vehicle (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX idx_2cb20dc545317d1 ON shipment (vehicle_id)');
|
||||||
|
}
|
||||||
|
}
|
||||||
49
migrations/Version20260211075656.php
Normal file
49
migrations/Version20260211075656.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260211075656 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment ON bovin_shipment (shipment_id, shipment_type_id)');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD user_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD driver_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD address_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCA76ED395 FOREIGN KEY (user_id) REFERENCES public."user" (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCC3423909 FOREIGN KEY (driver_id) REFERENCES driver (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCF5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCA76ED395 ON shipment (user_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCC3423909 ON shipment (driver_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_2CB20DCF5B7AF75 ON shipment (address_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP INDEX uniq_bovin_shipment');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCA76ED395');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCC3423909');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCF5B7AF75');
|
||||||
|
$this->addSql('DROP INDEX IDX_2CB20DCA76ED395');
|
||||||
|
$this->addSql('DROP INDEX IDX_2CB20DCC3423909');
|
||||||
|
$this->addSql('DROP INDEX IDX_2CB20DCF5B7AF75');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP user_id');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP driver_id');
|
||||||
|
$this->addSql('ALTER TABLE shipment DROP address_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
38
migrations/Version20260211123000.php
Normal file
38
migrations/Version20260211123000.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260211123000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Allow weight to belong to reception or shipment.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE weight ALTER COLUMN reception_id DROP NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE weight ADD shipment_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE weight ADD CONSTRAINT FK_WEIGHT_SHIPMENT FOREIGN KEY (shipment_id) REFERENCES shipment (id) NOT DEFERRABLE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_WEIGHT_SHIPMENT ON weight (shipment_id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_weight_reception_type ON weight (reception_id, type)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX uniq_weight_shipment_type ON weight (shipment_id, type)');
|
||||||
|
$this->addSql('ALTER TABLE weight ADD CONSTRAINT chk_weight_reception_or_shipment CHECK ((reception_id IS NOT NULL AND shipment_id IS NULL) OR (reception_id IS NULL AND shipment_id IS NOT NULL))');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE weight DROP CONSTRAINT chk_weight_reception_or_shipment');
|
||||||
|
$this->addSql('DROP INDEX uniq_weight_shipment_type');
|
||||||
|
$this->addSql('DROP INDEX uniq_weight_reception_type');
|
||||||
|
$this->addSql('DROP INDEX IDX_WEIGHT_SHIPMENT');
|
||||||
|
$this->addSql('ALTER TABLE weight DROP CONSTRAINT FK_WEIGHT_SHIPMENT');
|
||||||
|
$this->addSql('ALTER TABLE weight DROP shipment_id');
|
||||||
|
$this->addSql('ALTER TABLE weight ALTER COLUMN reception_id SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
migrations/Version20260213093000.php
Normal file
32
migrations/Version20260213093000.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213093000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add name, phone and email fields to customer.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ADD name VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD phone VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD email VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE customer SET name = label WHERE name IS NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN name SET NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer DROP name');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP phone');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP email');
|
||||||
|
}
|
||||||
|
}
|
||||||
37
migrations/Version20260213101500.php
Normal file
37
migrations/Version20260213101500.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260213101500 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Align customer with supplier: keep name/email/phone and drop label/code.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN name TYPE VARCHAR(180)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(180)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(40)');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP COLUMN label');
|
||||||
|
$this->addSql('ALTER TABLE customer DROP COLUMN code');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE customer ADD label VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ADD code VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE customer SET label = name WHERE label IS NULL');
|
||||||
|
$this->addSql("UPDATE customer SET code = regexp_replace(upper(name), '[^A-Z0-9]+', '_', 'g') WHERE code IS NULL");
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN label SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN code SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN email TYPE VARCHAR(255)');
|
||||||
|
$this->addSql('ALTER TABLE customer ALTER COLUMN phone TYPE VARCHAR(255)');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
@@ -223,6 +230,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' => 'Bovin de boucherie', 'code' => 'BDB'],
|
||||||
|
['label' => "Bovin d'é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 +498,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' => 'Rue de Nexon',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '87000',
|
||||||
|
'city' => 'LIMOGES',
|
||||||
|
'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.51.67.17.98',
|
||||||
|
'email' => null,
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'TERRENA',
|
||||||
|
'street' => 'La Blanchardière',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '44522',
|
||||||
|
'city' => 'MESANGER',
|
||||||
|
'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,6 +20,8 @@ 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'],
|
||||||
@@ -69,6 +74,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 +320,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' => 'Rue de Nexon',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '87000',
|
||||||
|
'city' => 'LIMOGES',
|
||||||
|
'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.51.67.17.98',
|
||||||
|
'email' => null,
|
||||||
|
'addresses' => [
|
||||||
|
[
|
||||||
|
'label' => 'TERRENA',
|
||||||
|
'street' => 'La Blanchardière',
|
||||||
|
'street2' => null,
|
||||||
|
'postalCode' => '44522',
|
||||||
|
'city' => 'MESANGER',
|
||||||
|
'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')
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
final readonly class PontBasculeReading
|
final readonly class PontBasculeReading
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Groups(['reception:weigh:read'])]
|
#[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
|
||||||
private ?int $dsd,
|
private ?int $dsd,
|
||||||
#[Groups(['reception:weigh:read'])]
|
#[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
|
||||||
private ?float $weight,
|
private ?float $weight,
|
||||||
#[Groups(['reception:weigh:read'])]
|
#[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
private ?DateTimeImmutable $weighedAt = null,
|
private ?DateTimeImmutable $weighedAt = null,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace App\Entity;
|
|||||||
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;
|
||||||
@@ -23,6 +25,16 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
new GetCollection(
|
new GetCollection(
|
||||||
normalizationContext: ['groups' => ['address:read']],
|
normalizationContext: ['groups' => ['address:read']],
|
||||||
),
|
),
|
||||||
|
new Post(
|
||||||
|
normalizationContext: ['groups' => ['address:read']],
|
||||||
|
denormalizationContext: ['groups' => ['address:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
normalizationContext: ['groups' => ['address:read']],
|
||||||
|
denormalizationContext: ['groups' => ['address:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('ROLE_USER')",
|
||||||
)]
|
)]
|
||||||
@@ -31,31 +43,31 @@ class Address
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['address:read', 'supplier:read'])]
|
#[Groups(['address:read', 'supplier:read', 'customer:read', 'shipment:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 120)]
|
#[ORM\Column(length: 120)]
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
||||||
private string $label = '';
|
private string $label = '';
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
||||||
private string $street = '';
|
private string $street = '';
|
||||||
|
|
||||||
#[ORM\Column(name: 'street2', length: 180, nullable: true)]
|
#[ORM\Column(name: 'street2', length: 180, nullable: true)]
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
||||||
private ?string $street2 = null;
|
private ?string $street2 = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'postal_code', length: 20)]
|
#[ORM\Column(name: 'postal_code', length: 20)]
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
||||||
private string $postalCode = '';
|
private string $postalCode = '';
|
||||||
|
|
||||||
#[ORM\Column(length: 120)]
|
#[ORM\Column(length: 120)]
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])]
|
||||||
private string $city = '';
|
private string $city = '';
|
||||||
|
|
||||||
#[ORM\Column(name: 'country_code', length: 2)]
|
#[ORM\Column(name: 'country_code', length: 2)]
|
||||||
#[Groups(['address:read', 'supplier:read'])]
|
#[Groups(['address:read', 'supplier:read', 'customer:read', 'address:write'])]
|
||||||
private string $countryCode = '';
|
private string $countryCode = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,9 +76,16 @@ class Address
|
|||||||
#[ORM\ManyToMany(targetEntity: Supplier::class, mappedBy: 'addresses')]
|
#[ORM\ManyToMany(targetEntity: Supplier::class, mappedBy: 'addresses')]
|
||||||
private Collection $suppliers;
|
private Collection $suppliers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Shipment>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Shipment::class, mappedBy: 'address')]
|
||||||
|
private Collection $shipments;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->suppliers = new ArrayCollection();
|
$this->suppliers = new ArrayCollection();
|
||||||
|
$this->shipments = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -146,7 +165,7 @@ class Address
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Groups(['address:read', 'supplier:read', 'reception:read'])]
|
#[Groups(['address:read', 'supplier:read', 'reception:read', 'shipment:read', 'customer:read'])]
|
||||||
public function getFullAddress(): string
|
public function getFullAddress(): string
|
||||||
{
|
{
|
||||||
$parts = array_filter([
|
$parts = array_filter([
|
||||||
@@ -165,4 +184,34 @@ class Address
|
|||||||
{
|
{
|
||||||
return $this->suppliers;
|
return $this->suppliers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Shipment>
|
||||||
|
*/
|
||||||
|
public function getShipments(): Collection
|
||||||
|
{
|
||||||
|
return $this->shipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addShipment(Shipment $shipment): static
|
||||||
|
{
|
||||||
|
if (!$this->shipments->contains($shipment)) {
|
||||||
|
$this->shipments->add($shipment);
|
||||||
|
$shipment->setAddress($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeShipment(Shipment $shipment): static
|
||||||
|
{
|
||||||
|
if ($this->shipments->removeElement($shipment)) {
|
||||||
|
// set the owning side to null (unless already changed)
|
||||||
|
if ($shipment->getAddress() === $this) {
|
||||||
|
$shipment->setAddress(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/Entity/BovinShipment.php
Normal file
101
src/Entity/BovinShipment.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['shipment' => 'exact'])]
|
||||||
|
#[ORM\UniqueConstraint(name: 'uniq_bovin_shipment', columns: ['shipment_id', 'shipment_type_id'])]
|
||||||
|
#[ORM\Table(name: 'bovin_shipment')]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
normalizationContext: ['groups' => ['shipment-bovine:read']],
|
||||||
|
),
|
||||||
|
new GetCollection(
|
||||||
|
normalizationContext: ['groups' => ['shipment-bovine:read']],
|
||||||
|
),
|
||||||
|
|
||||||
|
new Post(
|
||||||
|
normalizationContext: ['groups' => ['shipment-bovine:read']],
|
||||||
|
denormalizationContext: ['groups' => ['shipment-bovine:write']],
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
normalizationContext: ['groups' => ['shipment-bovine:read']],
|
||||||
|
denormalizationContext: ['groups' => ['shipment-bovine:write']],
|
||||||
|
),
|
||||||
|
new Delete(),
|
||||||
|
],
|
||||||
|
security: "is_granted('ROLE_USER')",
|
||||||
|
)]
|
||||||
|
class BovinShipment
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['shipment:read', 'shipment-bovine:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'bovinShipments')]
|
||||||
|
#[Groups(['shipment-bovine:read', 'shipment-bovine:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?Shipment $shipment = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[Groups(['shipment:read', 'shipment-bovine:write', 'shipment-bovine:read'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?ShipmentType $shipmentType = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['shipment:read', 'shipment-bovine:write', 'shipment-bovine:read'])]
|
||||||
|
private ?int $nbBovinSend = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShipment(): ?Shipment
|
||||||
|
{
|
||||||
|
return $this->shipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShipment(?Shipment $shipment): void
|
||||||
|
{
|
||||||
|
$this->shipment = $shipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShipmentType(): ?ShipmentType
|
||||||
|
{
|
||||||
|
return $this->shipmentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShipmentType(?ShipmentType $shipmentType): void
|
||||||
|
{
|
||||||
|
$this->shipmentType = $shipmentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNbBovinSend(): ?int
|
||||||
|
{
|
||||||
|
return $this->nbBovinSend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNbBovinSend(?int $nbBovinSend): void
|
||||||
|
{
|
||||||
|
$this->nbBovinSend = $nbBovinSend;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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']],
|
||||||
@@ -42,15 +44,15 @@ class Carrier
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['carrier:read', 'driver:read', 'vehicle:read', 'reception:read'])]
|
#[Groups(['carrier:read', 'driver:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read'])]
|
#[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
|
||||||
private string $name = '';
|
private string $name = '';
|
||||||
|
|
||||||
#[ORM\Column(length: 30, nullable: true)]
|
#[ORM\Column(length: 30, nullable: true)]
|
||||||
#[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read'])]
|
#[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
|
||||||
private ?string $code = null;
|
private ?string $code = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
|||||||
147
src/Entity/Customer.php
Normal file
147
src/Entity/Customer.php
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'customer')]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
normalizationContext: ['groups' => ['customer:read']],
|
||||||
|
),
|
||||||
|
new GetCollection(
|
||||||
|
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')",
|
||||||
|
)]
|
||||||
|
class Customer
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['shipment:read', 'customer:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 180)]
|
||||||
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
|
private string $name = '';
|
||||||
|
|
||||||
|
#[ORM\Column(length: 180, nullable: true)]
|
||||||
|
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||||
|
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>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'customers')]
|
||||||
|
#[ORM\JoinTable(name: 'customer_address')]
|
||||||
|
#[Groups(['customer:read', 'customer:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private Collection $addresses;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->addresses = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): self
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEmail(?string $email): self
|
||||||
|
{
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
return $this->addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAddresses(iterable $addresses): self
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,11 +30,11 @@ class Driver
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['driver:read', 'reception:read'])]
|
#[Groups(['driver:read', 'reception:read', 'shipment:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['driver:read', 'reception:read'])]
|
#[Groups(['driver:read', 'reception:read', 'shipment:read'])]
|
||||||
private string $name = '';
|
private string $name = '';
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
|
|||||||
369
src/Entity/Shipment.php
Normal file
369
src/Entity/Shipment.php
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
|
use App\Dto\PontBasculeReading;
|
||||||
|
use App\State\ShipmentReceiptProvider;
|
||||||
|
use App\State\ShipmentWeighingProvider;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Context;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\HasLifecycleCallbacks]
|
||||||
|
#[ORM\Table(name: 'shipment')]
|
||||||
|
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
normalizationContext: ['groups' => ['shipment:read']],
|
||||||
|
),
|
||||||
|
new GetCollection(
|
||||||
|
normalizationContext: ['groups' => ['shipment:read']],
|
||||||
|
),
|
||||||
|
new Post(
|
||||||
|
normalizationContext: ['groups' => ['shipment:read']],
|
||||||
|
denormalizationContext: ['groups' => ['shipment:write']],
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
normalizationContext: ['groups' => ['shipment:read']],
|
||||||
|
denormalizationContext: ['groups' => ['shipment:write']],
|
||||||
|
),
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/shipments/weigh',
|
||||||
|
openapi: new OpenApiOperation(
|
||||||
|
summary: 'Fetch the current weight reading',
|
||||||
|
description: 'Queries the pont-bascule and returns the weight data.',
|
||||||
|
),
|
||||||
|
normalizationContext: ['groups' => ['shipment:weigh:read']],
|
||||||
|
output: PontBasculeReading::class,
|
||||||
|
provider: ShipmentWeighingProvider::class,
|
||||||
|
),
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/shipments/{id}/receipt',
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
openapi: new OpenApiOperation(
|
||||||
|
summary: 'Render a shipment receipt',
|
||||||
|
description: 'Returns a PDF receipt for the shipment.',
|
||||||
|
),
|
||||||
|
output: false,
|
||||||
|
provider: ShipmentReceiptProvider::class,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
security: "is_granted('ROLE_USER')",
|
||||||
|
)]
|
||||||
|
class Shipment
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['shipment:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
private ?string $licencePlate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 20, unique: true, nullable: true)]
|
||||||
|
#[Groups(['shipment:read'])]
|
||||||
|
private ?string $identificationNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(options: ['default' => 0])]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
private int $currentStep = 0;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
private bool $isValid = false;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?User $user = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'shipment_date', type: 'datetime_immutable')]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
|
private ?DateTimeImmutable $shipmentDate = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?Carrier $carrier = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?Truck $truck = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?Customer $customer = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, BovinShipment>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(
|
||||||
|
targetEntity: BovinShipment::class,
|
||||||
|
mappedBy: 'shipment',
|
||||||
|
cascade: ['persist', 'remove'],
|
||||||
|
orphanRemoval: true
|
||||||
|
)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
private Collection $bovinShipments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Weight>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Weight::class, mappedBy: 'shipment', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
|
#[Groups(['shipment:read'])]
|
||||||
|
private Collection $weights;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?Driver $driver = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
#[Groups(['shipment:read', 'shipment:write'])]
|
||||||
|
#[ApiProperty(readableLink: true)]
|
||||||
|
private ?Address $address = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->bovinShipments = new ArrayCollection();
|
||||||
|
$this->weights = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLicencePlate(): ?string
|
||||||
|
{
|
||||||
|
return $this->licencePlate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLicencePlate(?string $licencePlate): void
|
||||||
|
{
|
||||||
|
$this->licencePlate = $licencePlate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdentificationNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->identificationNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIdentificationNumber(?string $identificationNumber): void
|
||||||
|
{
|
||||||
|
$this->identificationNumber = $identificationNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentStep(): int
|
||||||
|
{
|
||||||
|
return $this->currentStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCurrentStep(int $currentStep): void
|
||||||
|
{
|
||||||
|
$this->currentStep = $currentStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIsValid(): ?bool
|
||||||
|
{
|
||||||
|
return $this->isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Groups(['shipment:read'])]
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return $this->isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(?User $user): void
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIsValid(?bool $isValid): void
|
||||||
|
{
|
||||||
|
$this->isValid = $isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShipmentDate(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->shipmentDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShipmentDate(?DateTimeImmutable $shipmentDate): void
|
||||||
|
{
|
||||||
|
$this->shipmentDate = $shipmentDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCarrier(): ?Carrier
|
||||||
|
{
|
||||||
|
return $this->carrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCarrier(?Carrier $carrier): void
|
||||||
|
{
|
||||||
|
$this->carrier = $carrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTruck(): ?Truck
|
||||||
|
{
|
||||||
|
return $this->truck;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTruck(?Truck $truck): void
|
||||||
|
{
|
||||||
|
$this->truck = $truck;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomer(): ?Customer
|
||||||
|
{
|
||||||
|
return $this->customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomer(?Customer $customer): void
|
||||||
|
{
|
||||||
|
$this->customer = $customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBovinShipments(): Collection
|
||||||
|
{
|
||||||
|
return $this->bovinShipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBovinShipments(Collection $bovinShipments): void
|
||||||
|
{
|
||||||
|
$this->bovinShipments = $bovinShipments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addBovinShipment(BovinShipment $bovinShipment): self
|
||||||
|
{
|
||||||
|
if (!$this->bovinShipments->contains($bovinShipment)) {
|
||||||
|
$this->bovinShipments->add($bovinShipment);
|
||||||
|
$bovinShipment->setShipment($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeBovinShipment(BovinShipment $bovinShipment): self
|
||||||
|
{
|
||||||
|
if ($this->bovinShipments->removeElement($bovinShipment)) {
|
||||||
|
if ($bovinShipment->getShipment() === $this) {
|
||||||
|
$bovinShipment->setShipment(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Weight>
|
||||||
|
*/
|
||||||
|
public function getWeights(): Collection
|
||||||
|
{
|
||||||
|
return $this->weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addWeight(Weight $weight): void
|
||||||
|
{
|
||||||
|
if (!$this->weights->contains($weight)) {
|
||||||
|
$this->weights->add($weight);
|
||||||
|
$weight->setShipment($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeWeight(Weight $weight): void
|
||||||
|
{
|
||||||
|
if ($this->weights->removeElement($weight)) {
|
||||||
|
if ($weight->getShipment() === $this) {
|
||||||
|
$weight->setShipment(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ORM\PostPersist]
|
||||||
|
public function initializeIdentificationNumber(PostPersistEventArgs $args): void
|
||||||
|
{
|
||||||
|
if (null !== $this->identificationNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $this->id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = sprintf('P-BR-%04d', $this->id);
|
||||||
|
$this->identificationNumber = $number;
|
||||||
|
|
||||||
|
$args->getObjectManager()
|
||||||
|
->getConnection()
|
||||||
|
->executeStatement(
|
||||||
|
'UPDATE shipment SET identification_number = :number WHERE id = :id',
|
||||||
|
[
|
||||||
|
'number' => $number,
|
||||||
|
'id' => $this->id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDriver(): ?Driver
|
||||||
|
{
|
||||||
|
return $this->driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDriver(?Driver $driver): static
|
||||||
|
{
|
||||||
|
$this->driver = $driver;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAddress(): ?Address
|
||||||
|
{
|
||||||
|
return $this->address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAddress(?Address $address): static
|
||||||
|
{
|
||||||
|
$this->address = $address;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Entity/ShipmentType.php
Normal file
71
src/Entity/ShipmentType.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'shipment_type')]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
requirements: ['id' => '\d+'],
|
||||||
|
normalizationContext: ['groups' => ['shipment-type:read']],
|
||||||
|
),
|
||||||
|
new GetCollection(
|
||||||
|
normalizationContext: ['groups' => ['shipment-type:read']],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
security: "is_granted('ROLE_USER')",
|
||||||
|
)]
|
||||||
|
class ShipmentType
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['shipment-type:read', 'shipment:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Groups(['shipment-type:read', 'shipment:read'])]
|
||||||
|
private ?string $label = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Groups(['shipment-type:read', 'shipment:read'])]
|
||||||
|
private ?string $code = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabel(): ?string
|
||||||
|
{
|
||||||
|
return $this->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLabel(string $label): static
|
||||||
|
{
|
||||||
|
$this->label = $label;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCode(string $code): self
|
||||||
|
{
|
||||||
|
$this->code = $code;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -20,9 +22,21 @@ 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_ADMIN')"
|
||||||
|
),
|
||||||
|
new Post(
|
||||||
|
normalizationContext: ['groups' => ['supplier:read']],
|
||||||
|
denormalizationContext: ['groups' => ['supplier:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
|
),
|
||||||
|
new Patch(
|
||||||
|
normalizationContext: ['groups' => ['supplier:read']],
|
||||||
|
denormalizationContext: ['groups' => ['supplier:write']],
|
||||||
|
security: "is_granted('ROLE_ADMIN')",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('ROLE_USER')",
|
||||||
@@ -36,15 +50,15 @@ class Supplier
|
|||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['supplier:read', 'reception:read'])]
|
#[Groups(['supplier:read', 'reception:read', 'supplier:write'])]
|
||||||
private string $name = '';
|
private string $name = '';
|
||||||
|
|
||||||
#[ORM\Column(length: 180, nullable: true)]
|
#[ORM\Column(length: 180, nullable: true)]
|
||||||
#[Groups(['supplier:read', 'reception:read'])]
|
#[Groups(['supplier:read', 'reception:read', 'supplier:write'])]
|
||||||
private ?string $email = null;
|
private ?string $email = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 40, nullable: true)]
|
#[ORM\Column(length: 40, nullable: true)]
|
||||||
#[Groups(['supplier:read', 'reception:read'])]
|
#[Groups(['supplier:read', 'reception:read', 'supplier:write'])]
|
||||||
private ?string $phone = null;
|
private ?string $phone = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +66,7 @@ class Supplier
|
|||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'suppliers')]
|
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'suppliers')]
|
||||||
#[ORM\JoinTable(name: 'supplier_address')]
|
#[ORM\JoinTable(name: 'supplier_address')]
|
||||||
#[Groups(['supplier:read'])]
|
#[Groups(['supplier:read', 'supplier:write'])]
|
||||||
#[ApiProperty(readableLink: true)]
|
#[ApiProperty(readableLink: true)]
|
||||||
private Collection $addresses;
|
private Collection $addresses;
|
||||||
|
|
||||||
@@ -109,4 +123,30 @@ class Supplier
|
|||||||
{
|
{
|
||||||
return $this->addresses;
|
return $this->addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setAddresses(iterable $addresses): self
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ class Truck
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['truck:read', 'vehicle:read', 'reception:read'])]
|
#[Groups(['truck:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['truck:read', 'vehicle:read', 'reception:read'])]
|
#[Groups(['truck:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
|
||||||
private string $name = '';
|
private string $name = '';
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column(type: 'integer')]
|
#[ORM\Column(type: 'integer')]
|
||||||
#[Groups(['user:read', 'user-login:read', 'reception:read'])]
|
#[Groups(['user:read', 'user-login:read', 'reception:read', 'shipment:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180, unique: true)]
|
#[ORM\Column(length: 180, unique: true)]
|
||||||
#[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read'])]
|
#[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read', 'shipment:read'])]
|
||||||
private string $username = '';
|
private string $username = '';
|
||||||
|
|
||||||
#[ORM\Column(type: 'json')]
|
#[ORM\Column(type: 'json')]
|
||||||
|
|||||||
@@ -35,36 +35,46 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
security: "is_granted('ROLE_USER')",
|
security: "is_granted('ROLE_USER')",
|
||||||
)]
|
)]
|
||||||
#[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')]
|
#[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')]
|
||||||
|
#[UniqueEntity(fields: ['shipment', 'type'], message: 'A weighing already exists for this type.')]
|
||||||
|
#[Assert\Expression(
|
||||||
|
'(this.getReception() !== null and this.getShipment() === null) or (this.getReception() === null and this.getShipment() !== null)',
|
||||||
|
message: 'Either reception or shipment must be set, but not both.'
|
||||||
|
)]
|
||||||
class Weight
|
class Weight
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
#[Groups(['reception:read', 'weight:read'])]
|
#[Groups(['reception:read', 'shipment:read', 'weight:read'])]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'weights')]
|
#[ORM\ManyToOne(inversedBy: 'weights')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
#[Groups(['weight:read', 'weight:write'])]
|
#[Groups(['weight:read', 'weight:write'])]
|
||||||
private ?Reception $reception = null;
|
private ?Reception $reception = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'weights')]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
#[Groups(['weight:read', 'weight:write'])]
|
||||||
|
private ?Shipment $shipment = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
#[Groups(['reception:read', 'weight:read', 'weight:write'])]
|
#[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
|
||||||
#[Assert\PositiveOrZero]
|
#[Assert\PositiveOrZero]
|
||||||
private ?int $dsd = null;
|
private ?int $dsd = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
#[Groups(['reception:read', 'weight:read', 'weight:write'])]
|
#[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
|
||||||
#[Assert\PositiveOrZero]
|
#[Assert\PositiveOrZero]
|
||||||
private ?int $weight = null;
|
private ?int $weight = null;
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||||
#[Groups(['reception:read', 'weight:read', 'weight:write'])]
|
#[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
private ?DateTimeImmutable $weighedAt = null;
|
private ?DateTimeImmutable $weighedAt = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 10)]
|
#[ORM\Column(length: 10)]
|
||||||
#[Groups(['reception:read', 'weight:read', 'weight:write'])]
|
#[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
|
||||||
#[Assert\NotBlank]
|
#[Assert\NotBlank]
|
||||||
#[Assert\Choice(choices: ['gross', 'tare'])]
|
#[Assert\Choice(choices: ['gross', 'tare'])]
|
||||||
private string $type = 'gross';
|
private string $type = 'gross';
|
||||||
@@ -90,6 +100,22 @@ class Weight
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getShipment(): ?Shipment
|
||||||
|
{
|
||||||
|
return $this->shipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShipment(?Shipment $shipment): self
|
||||||
|
{
|
||||||
|
$this->shipment = $shipment;
|
||||||
|
|
||||||
|
if (null !== $shipment && !$shipment->getWeights()->contains($this)) {
|
||||||
|
$shipment->addWeight($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getDsd(): ?int
|
public function getDsd(): ?int
|
||||||
{
|
{
|
||||||
return $this->dsd;
|
return $this->dsd;
|
||||||
|
|||||||
63
src/State/ShipmentReceiptProvider.php
Normal file
63
src/State/ShipmentReceiptProvider.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Entity\Shipment;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Dompdf\Dompdf;
|
||||||
|
use Dompdf\Options;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Error\LoaderError;
|
||||||
|
use Twig\Error\RuntimeError;
|
||||||
|
use Twig\Error\SyntaxError;
|
||||||
|
|
||||||
|
final readonly class ShipmentReceiptProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Environment $twig,
|
||||||
|
private EntityManagerInterface $entityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeError
|
||||||
|
* @throws SyntaxError
|
||||||
|
* @throws LoaderError
|
||||||
|
*/
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
||||||
|
{
|
||||||
|
$id = $uriVariables['id'] ?? null;
|
||||||
|
if (null === $id) {
|
||||||
|
throw new NotFoundHttpException('Shipment not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$shipment = $this->entityManager->getRepository(Shipment::class)->find($id);
|
||||||
|
if (!$shipment instanceof Shipment) {
|
||||||
|
throw new NotFoundHttpException('Shipment not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = new Options();
|
||||||
|
$options->set('isRemoteEnabled', true);
|
||||||
|
|
||||||
|
$dompdf = new Dompdf($options);
|
||||||
|
$html = $this->twig->render('shipment_voucher.html.twig', [
|
||||||
|
'shipment' => $shipment,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dompdf->loadHtml($html);
|
||||||
|
$dompdf->setPaper('A4');
|
||||||
|
$dompdf->render();
|
||||||
|
|
||||||
|
$filename = sprintf('bon-expedition-%d.pdf', $shipment->getId());
|
||||||
|
|
||||||
|
return new Response($dompdf->output(), Response::HTTP_OK, [
|
||||||
|
'Content-Type' => 'application/pdf',
|
||||||
|
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/State/ShipmentWeighingProvider.php
Normal file
30
src/State/ShipmentWeighingProvider.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Dto\PontBasculeReading;
|
||||||
|
use App\Exception\PontBasculeException;
|
||||||
|
use App\Service\PontBasculeService;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
final readonly class ShipmentWeighingProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PontBasculeService $pontBasculeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?PontBasculeReading
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->pontBasculeService->fetch();
|
||||||
|
} catch (PontBasculeException $exception) {
|
||||||
|
throw new HttpException(500, $exception->getMessage(), $exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -141,16 +141,18 @@
|
|||||||
|
|
||||||
<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>{{ reception.supplier.name }}</strong><br>
|
<strong>{{ reception.supplier ? reception.supplier.name : '-' }}</strong><br>
|
||||||
<span>{{ reception.address.street }}</span><br>
|
<span>{{ reception.address ? reception.address.street : '' }}</span><br>
|
||||||
{% if reception.address.street2 %}
|
{% if reception.address and reception.address.street2 %}
|
||||||
<span>{{ reception.address.street2 }}</span><br>
|
<span>{{ reception.address.street2 }}</span><br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>{{ reception.address.postalCode }} {{ reception.address.city }}</span><br>
|
{% if reception.address %}
|
||||||
{% if reception.supplier.phone %}
|
<span>{{ reception.address.postalCode }} {{ reception.address.city }}</span><br>
|
||||||
|
{% endif %}
|
||||||
|
{% if reception.supplier and reception.supplier.phone %}
|
||||||
<span>{{ reception.supplier.phone }}</span><br>
|
<span>{{ reception.supplier.phone }}</span><br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if reception.supplier.email %}
|
{% if reception.supplier and reception.supplier.email %}
|
||||||
<span>{{ reception.supplier.email}}</span><br>
|
<span>{{ reception.supplier.email}}</span><br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +170,9 @@
|
|||||||
<th style="width:25%; text-align:center; white-space:nowrap;">N° réception</th>
|
<th style="width:25%; text-align:center; white-space:nowrap;">N° réception</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:55%; text-align:center;">{{ reception.supplier.name }}</td>
|
<td style="width:55%; text-align:center;">
|
||||||
|
{{ reception.supplier ? reception.supplier.name : '-' }}
|
||||||
|
</td>
|
||||||
<td style="width:20%; text-align:center; white-space:nowrap;">
|
<td style="width:20%; text-align:center; white-space:nowrap;">
|
||||||
{{ reception.receptionDate|date('d/m/Y') }}
|
{{ reception.receptionDate|date('d/m/Y') }}
|
||||||
</td>
|
</td>
|
||||||
@@ -189,13 +193,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:75%;">
|
<td style="width:75%;">
|
||||||
<strong>{{ reception.receptionType.label }}</strong><br><br>
|
<strong>{{ reception.receptionType ? reception.receptionType.label : '-' }}</strong><br><br>
|
||||||
|
|
||||||
<div class="bigtable-notes">
|
<div class="bigtable-notes">
|
||||||
{% set grossWeight = null %}
|
{% set grossWeight = null %}
|
||||||
{% set tareWeight = null %}
|
{% set tareWeight = null %}
|
||||||
|
{% for weight in reception.weights|default([]) %}
|
||||||
{% for weight in reception.weights %}
|
|
||||||
{% if weight.type == 'gross' %}
|
{% if weight.type == 'gross' %}
|
||||||
{% set grossWeight = weight %}
|
{% set grossWeight = weight %}
|
||||||
<p>Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
|
<p>Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
|
||||||
@@ -219,45 +221,57 @@
|
|||||||
<tr class="border-bottom">
|
<tr class="border-bottom">
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
{% if reception.merchandiseType %}
|
Type de bovins
|
||||||
{{ reception.merchandiseType.label }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</strong>
|
</strong>
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
<div class="bigtable-notes">
|
<div class="bigtable-notes">
|
||||||
{% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %}
|
{% if reception.receptionType and reception.receptionType.code == 'BOVINS' %}
|
||||||
<p><strong>Précision</strong> : {{ reception.merchandiseDetail }}</p>
|
{% if reception.bovinesTypes is not empty %}
|
||||||
{% endif %}
|
{% for entry in reception.bovinesTypes %}
|
||||||
|
<p>
|
||||||
{% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %}
|
{{ entry.bovineType ? entry.bovineType.label : '-' }} : {{ entry.quantity ?? 0 }}
|
||||||
{% set pelletGroups = {} %}
|
</p>
|
||||||
{% for selection in reception.pelletBuildings %}
|
{% endfor %}
|
||||||
{% set pelletLabel = selection.pelletType.label %}
|
|
||||||
{% if pelletGroups[pelletLabel] is not defined %}
|
|
||||||
{% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %}
|
|
||||||
{% endif %}
|
|
||||||
{% set pelletGroups = pelletGroups|merge({
|
|
||||||
(pelletLabel): pelletGroups[pelletLabel]|merge([selection.building.label])
|
|
||||||
}) %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for pelletLabel, buildingLabels in pelletGroups %}
|
|
||||||
<p><strong>{{ pelletLabel }}</strong> : {{ buildingLabels|join(', ') }}</p>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Aucun dépôt de granulés renseigné.</p>
|
<p>-</p>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if reception.bovineDetail %}
|
||||||
|
<p>Autres : {{ reception.bovineDetail }}</p>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set buildingLabels = [] %}
|
{% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %}
|
||||||
{% for building in reception.buildings %}
|
<p><strong>Précision</strong> : {{ reception.merchandiseDetail }}</p>
|
||||||
{% set buildingLabels = buildingLabels|merge([building.label]) %}
|
{% endif %}
|
||||||
{% endfor %}
|
|
||||||
{% if buildingLabels %}
|
{% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %}
|
||||||
<p><strong>Ferme</strong> : {{ buildingLabels|join(', ') }}</p>
|
{% set pelletGroups = {} %}
|
||||||
|
{% for selection in reception.pelletBuildings|default([]) %}
|
||||||
|
{% set pelletLabel = selection.pelletType.label %}
|
||||||
|
{% if pelletGroups[pelletLabel] is not defined %}
|
||||||
|
{% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %}
|
||||||
|
{% endif %}
|
||||||
|
{% set pelletGroups = pelletGroups|merge({
|
||||||
|
(pelletLabel): pelletGroups[pelletLabel]|merge([selection.building.label])
|
||||||
|
}) %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for pelletLabel, buildingLabels in pelletGroups %}
|
||||||
|
<p><strong>{{ pelletLabel }}</strong> : {{ buildingLabels|join(', ') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Aucun dépôt de granulés renseigné.</p>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Aucun bâtiment renseigné.</p>
|
{% set buildingLabels = [] %}
|
||||||
|
{% for building in reception.buildings|default([]) %}
|
||||||
|
{% set buildingLabels = buildingLabels|merge([building.label]) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if buildingLabels %}
|
||||||
|
<p><strong>Ferme</strong> : {{ buildingLabels|join(', ') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Aucun bâtiment renseigné.</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -273,9 +287,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<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">
|
||||||
Transporteur : <strong>{{ reception.carrier.name }}</strong><br>
|
Transporteur : <strong>{{ reception.carrier ? reception.carrier.name : '-' }}</strong><br>
|
||||||
Mode de livraison : <strong>{{ reception.truck.name }}</strong><br>
|
Mode de livraison : <strong>{{ reception.truck ? reception.truck.name : '-' }}</strong><br>
|
||||||
Immatriculation : <strong>{{ reception.licensePlate }}</strong><br><br>
|
Immatriculation : <strong>{{ reception.licensePlate ?? '-' }}</strong><br><br>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|||||||
292
templates/shipment_voucher.html.twig
Normal file
292
templates/shipment_voucher.html.twig
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
margin: 56px 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-block {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18pt;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 64px 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 4px 6px;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout, .layout td {
|
||||||
|
border: none !important;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable-wrap {
|
||||||
|
border: 1px solid #000;
|
||||||
|
height: 360px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable th,
|
||||||
|
.bigtable td {
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable thead th {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable tbody tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable tr th:first-child,
|
||||||
|
.bigtable tr td:first-child {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable tr th:last-child,
|
||||||
|
.bigtable tr td:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable thead th {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable tbody tr:first-child td {
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigtable-notes {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-bottom {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-block {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-box {
|
||||||
|
height: 130px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 0.5px solid #000;
|
||||||
|
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<table class="layout" style="width:100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="width:70%; vertical-align:top;">
|
||||||
|
<table class="layout" style="width:100%;">
|
||||||
|
<tr>
|
||||||
|
<td class="company-block" style="padding:0; border:none;">
|
||||||
|
<strong>SCEA LES NAUDS</strong><br>
|
||||||
|
14 Allée d’Argenson<br>
|
||||||
|
Z.I Nord – Secteur Est<br>
|
||||||
|
86100 CHATELLERAULT<br>
|
||||||
|
Tel. : 05 49 20 09 10<br>
|
||||||
|
Email : lpc.contacts@lpc-liot.fr<br>
|
||||||
|
RCS Châtellerault B 444 262 455
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td style="width:30%; text-align:right; vertical-align:top; font-size: 14px;">
|
||||||
|
<div style="display:inline-block; width:75mm; line-height:1.3;">
|
||||||
|
<strong>{{ shipment.customer ? shipment.customer.label : '-' }}</strong><br>
|
||||||
|
<span>{{ shipment.address ? shipment.address.street : '' }}</span><br>
|
||||||
|
{% if shipment.address and shipment.address.street2 %}
|
||||||
|
<span>{{ shipment.address.street2 }}</span><br>
|
||||||
|
{% endif %}
|
||||||
|
{% if shipment.address %}
|
||||||
|
<span>{{ shipment.address.postalCode }} {{ shipment.address.city }}</span><br>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="title">BON D'EXPEDITION</div>
|
||||||
|
|
||||||
|
<!-- INFOS (code/date/num) -->
|
||||||
|
<table class="info-table">
|
||||||
|
<tr>
|
||||||
|
<th style="width:55%; text-align:center;">Code client</th>
|
||||||
|
<th style="width:20%; text-align:center; white-space:nowrap;">Date</th>
|
||||||
|
<th style="width:25%; text-align:center; white-space:nowrap;">N° expédition</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="width:55%; text-align:center;">
|
||||||
|
{{ shipment.customer ? shipment.customer.code : '-' }}
|
||||||
|
</td>
|
||||||
|
<td style="width:20%; text-align:center; white-space:nowrap;">
|
||||||
|
{{ shipment.shipmentDate|date('d/m/Y') }}
|
||||||
|
</td>
|
||||||
|
<td style="width:25%; text-align:center; white-space:nowrap;">
|
||||||
|
{{ shipment.identificationNumber ?? '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- GRAND TABLEAU -->
|
||||||
|
<div class="bigtable-wrap">
|
||||||
|
<table class="bigtable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:75%; text-align:center;">Désignation</th>
|
||||||
|
<th style="width:25%; text-align:center; white-space:nowrap;">Qté expédiée (kg)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% set grossWeight = null %}
|
||||||
|
{% set tareWeight = null %}
|
||||||
|
<tr>
|
||||||
|
<td style="width:75%;">
|
||||||
|
<strong>Expédition</strong><br><br>
|
||||||
|
<div class="bigtable-notes">
|
||||||
|
{% for weight in shipment.weights %}
|
||||||
|
{% if weight.type == 'gross' %}
|
||||||
|
{% set grossWeight = weight %}
|
||||||
|
<p>Poids à plein : {{ grossWeight.weight }}kg (pesée
|
||||||
|
n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
|
||||||
|
{% elseif weight.type == 'tare' %}
|
||||||
|
{% set tareWeight = weight %}
|
||||||
|
<p>Poids à vide : {{ tareWeight.weight }}kg (pesée
|
||||||
|
n°{{ tareWeight.dsd }} {{ tareWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="width:25%; text-align:center; white-space:nowrap;">
|
||||||
|
{% if grossWeight and tareWeight %}
|
||||||
|
{{ grossWeight.weight - tareWeight.weight }}
|
||||||
|
{% else %}
|
||||||
|
0
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-bottom">
|
||||||
|
<td>
|
||||||
|
<strong>Bovin</strong><br><br>
|
||||||
|
<div class="bigtable-notes">
|
||||||
|
{% if shipment.bovinShipments is not empty %}
|
||||||
|
{% for entry in shipment.bovinShipments %}
|
||||||
|
<p>
|
||||||
|
{{ entry.shipmentType ? entry.shipmentType.label : '-' }} :
|
||||||
|
{{ entry.nbBovinSend ?? 0 }}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>-</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td style="width:25%; text-align:center; white-space:nowrap;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BAS : meta à gauche / signatures à droite -->
|
||||||
|
<table class="layout footer-block">
|
||||||
|
<tr>
|
||||||
|
<td style="width:60%; padding-right:8mm; vertical-align:top;">
|
||||||
|
<div class="meta">
|
||||||
|
<p>Transporteur : {{ shipment.carrier ? shipment.carrier.name : '-' }}</p>
|
||||||
|
<p>Mode de livraison : {{ shipment.truck ? shipment.truck.name : '-' }}</p>
|
||||||
|
<p>Immatriculation : {{ shipment.licencePlate ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td style="width:40%; vertical-align:top;">
|
||||||
|
<div class="box signature-box">Signature les Nauds :</div>
|
||||||
|
<div class="box signature-box">Signature transporteur :</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user