Compare commits
6 Commits
feat/276-l
...
v0.0.44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c361ef9bb9 | ||
| 7f3d9ef9c6 | |||
|
|
22b959de85 | ||
| d3bc2e11f1 | |||
|
|
d8b16f5e15 | ||
| 43213bc6d6 |
10
.idea/data_source_mapping.xml
generated
10
.idea/data_source_mapping.xml
generated
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_1.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_2.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_3.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/f407a514-c6b4-4b26-9555-445a85892502/console_4.sql" value="f407a514-c6b4-4b26-9555-445a85892502" />
|
||||
</component>
|
||||
</project>
|
||||
216
.idea/workspace.xml
generated
216
.idea/workspace.xml
generated
@@ -4,28 +4,10 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)">
|
||||
<change afterPath="$PROJECT_DIR$/frontend/components/shipment/shipment-form.vue" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/pages/shipment/[[id]].vue" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/bovin-shipment.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/customer.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/bovin-shipment-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/customer-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/shipment-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/dto/shipment-type-data.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/shipment-type.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/services/shipment.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/frontend/stores/shipment.ts" afterDir="false" />
|
||||
<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$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/ui/UiNumberInput.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/constants/steps.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/constants/steps.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/index.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/services/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/Address.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Address.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/BovinShipment.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/BovinShipment.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/Shipment.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Shipment.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -50,17 +32,21 @@
|
||||
<list>
|
||||
<option value="Vue Composition API Component" />
|
||||
<option value="TypeScript File" />
|
||||
<option value="PHP File" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="fix/makefile" />
|
||||
<entry key="$PROJECT_DIR$" value="feat/276-lister-expeditions-terminees" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$PROJECT_DIR$/frontend/pages/admin/supplier/supplier-list.vue" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="McpProjectServerCommands">
|
||||
<commands />
|
||||
<urls />
|
||||
@@ -237,37 +223,42 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"git-widget-placeholder": "feat/271-expedition-etape-1",
|
||||
"last_opened_file_path": "/home/sroy/Documents/test/Ferme",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "configurable.tailwindcss",
|
||||
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"git-widget-placeholder": "fix/325-corrections-diverses",
|
||||
"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.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
],
|
||||
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||
"TEXT"
|
||||
"com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
|
||||
"TEXT"
|
||||
],
|
||||
"vue.recent.templates": [
|
||||
"Vue Composition API Component"
|
||||
"vue.recent.templates": [
|
||||
"Vue Composition API Component"
|
||||
]
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<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">
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||
@@ -313,54 +304,10 @@
|
||||
<workItem from="1770195718952" duration="215000" />
|
||||
<workItem from="1770195959162" duration="18915000" />
|
||||
<workItem from="1770274844804" duration="3940000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768237763998</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768237763998</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768316052474</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768316052474</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768316835575</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768316835575</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00004" summary="feat : update du fichier AGENTS.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768316965511</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768316965511</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00005" summary="feat : update du fichier README.md et CHANGELOG.md">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768317786187</created>
|
||||
<option name="number" value="00005" />
|
||||
<option name="presentableId" value="LOCAL-00005" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768317786187</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="fix : correction du useApi pour qu'il n'y ait plus de retry lors d'une erreur 500 par exemple">
|
||||
<option name="closed" value="true" />
|
||||
<created>1768318875533</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1768318875533</updated>
|
||||
<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" />
|
||||
@@ -706,7 +653,55 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770217875423</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="50" />
|
||||
<task id="LOCAL-00050" summary="feat : creer une nouvelle expedtion (WIP)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770736570645</created>
|
||||
<option name="number" value="00050" />
|
||||
<option name="presentableId" value="LOCAL-00050" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770736570645</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00051" summary="feat : ajout d'une page de creation d'une expedition">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770880791564</created>
|
||||
<option name="number" value="00051" />
|
||||
<option name="presentableId" value="LOCAL-00051" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770880791565</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00052" summary="feat : changelog">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770881437439</created>
|
||||
<option name="number" value="00052" />
|
||||
<option name="presentableId" value="LOCAL-00052" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770881437439</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00053" summary="feat : lister les expeditions terminees">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770883114609</created>
|
||||
<option name="number" value="00053" />
|
||||
<option name="presentableId" value="LOCAL-00053" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770883114609</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00054" summary="feat : lister les expeditions terminees">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770884154297</created>
|
||||
<option name="number" value="00054" />
|
||||
<option name="presentableId" value="LOCAL-00054" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770884154297</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00055" summary="fix : corrections diverses">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770969471135</created>
|
||||
<option name="number" value="00055" />
|
||||
<option name="presentableId" value="LOCAL-00055" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770969471135</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="56" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -756,12 +751,6 @@
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="fix : correction du path URI pour la création d'un poids dans une réception" />
|
||||
<MESSAGE value="feat : Ajout du bundle Monolog pour la gestion des logs" />
|
||||
<MESSAGE value="fix : affiche plus détail dans les logs en recette/prod" />
|
||||
<MESSAGE value="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod" />
|
||||
<MESSAGE value="fix : doc de déploiement" />
|
||||
<MESSAGE value="fix : doc et script de déploiement" />
|
||||
<MESSAGE value="fix : gitea workflow" />
|
||||
<MESSAGE value="fix : 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" />
|
||||
@@ -781,7 +770,13 @@
|
||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||
<MESSAGE value="feat : creer une nouvelle expedtion (WIP)" />
|
||||
<MESSAGE value="feat : ajout d'une page de creation d'une expedition" />
|
||||
<MESSAGE value="feat : changelog" />
|
||||
<MESSAGE value="feat : lister les expeditions terminees" />
|
||||
<MESSAGE value="fix: corrections diverses" />
|
||||
<MESSAGE value="fix : corrections diverses" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix : corrections diverses" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
@@ -791,10 +786,19 @@
|
||||
<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/shipment.ts</url>
|
||||
<properties lambdaOrdinal="-1" />
|
||||
<option name="timeStamp" value="37" />
|
||||
<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>
|
||||
|
||||
@@ -43,6 +43,9 @@ Ajouter dans le fichier .env du frontend
|
||||
* [#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
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.0.41'
|
||||
app.version: '0.0.44'
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<button
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
@click="goNext"
|
||||
>Peser
|
||||
>Valider
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||
>Peser
|
||||
>Valider
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -342,7 +342,7 @@ onMounted(async () => {
|
||||
|
||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||
watch(
|
||||
() => [form.supplierId, suppliers.value],
|
||||
() => [form.supplierId, form.addressId, suppliers.value],
|
||||
() => {
|
||||
if (!form.supplierId) {
|
||||
form.addressId = ''
|
||||
@@ -359,7 +359,11 @@ watch(
|
||||
(address) => String(address.id) === form.addressId
|
||||
)
|
||||
if (!matches) {
|
||||
form.addressId = ''
|
||||
if (supplierAddresses.value.length === 1) {
|
||||
form.addressId = String(supplierAddresses.value[0].id)
|
||||
} else {
|
||||
form.addressId = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<button
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
@click="goNext"
|
||||
>Peser
|
||||
>Valider
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
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>
|
||||
>Valider</button>
|
||||
<button
|
||||
v-if="showGenerateReceipt"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import {computed, onMounted} from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useWeighing } from '~/composables/useWeighing'
|
||||
import { usePdfPrinter } from '~/composables/usePdfPrinter'
|
||||
@@ -94,7 +94,7 @@ const printReceipt = async () => {
|
||||
|
||||
// Récupère le poids dès l'arrivée sur l'écran
|
||||
onMounted(() => {
|
||||
if (false === displayWeight.value) {
|
||||
if (displayWeight.value === null) {
|
||||
fetchWeight()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,79 +1,80 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
<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 class="flex flex-col items-center gap-16">
|
||||
<div
|
||||
v-if="merchandiseTypeId && isAutres"
|
||||
class="flex flex-col w-full max-w-[550px]"
|
||||
>
|
||||
class="flex flex-col gap-16 items-center w-full">
|
||||
<UiTextInput
|
||||
id="merchandise-detail"
|
||||
:disabled="!auth.isAdmin"
|
||||
v-model="merchandiseDetail"
|
||||
label="Préciser"
|
||||
placeholder="Précisions complémentaires"
|
||||
:maxlength="255"
|
||||
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 && !isGranule"
|
||||
class="flex gap-4 w-[550px] justify-evenly"
|
||||
>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
v-if="merchandiseTypeId && isAutres"
|
||||
class="flex flex-col w-full max-w-[550px]"
|
||||
>
|
||||
<UiCheckbox
|
||||
v-model="selectedBuildingIds"
|
||||
:value="String(building.id)"
|
||||
:label="building.label"
|
||||
<UiTextInput
|
||||
id="merchandise-detail"
|
||||
:disabled="!auth.isAdmin"
|
||||
label-class="text-xl"
|
||||
v-model="merchandiseDetail"
|
||||
label="Préciser"
|
||||
placeholder="Précisions complémentaires"
|
||||
:maxlength="255"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
v-if="merchandiseTypeId && !isGranule"
|
||||
class="flex gap-4 w-[550px] justify-evenly"
|
||||
>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
>
|
||||
<UiCheckbox
|
||||
v-model="selectedBuildingIds"
|
||||
:value="String(building.id)"
|
||||
:label="building.label"
|
||||
:disabled="!auth.isAdmin"
|
||||
label-class="text-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
:disabled="!auth.isAdmin"
|
||||
>Valider
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-16">
|
||||
<UiNumberInput
|
||||
label="Pesée à vide"
|
||||
v-model="form.weights[0].weight"
|
||||
<div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-8">
|
||||
<UiTextInput
|
||||
label="Dsd"
|
||||
class="col-start-2"
|
||||
v-model="sharedWeightMeta.dsd"
|
||||
:disabled="!auth.isAdmin"
|
||||
:min="0"
|
||||
/>
|
||||
|
||||
<UiNumberInput
|
||||
label="Pesée à plein"
|
||||
v-model="form.weights[1].weight"
|
||||
<UiDateInput
|
||||
label="Date pesée"
|
||||
v-model="sharedWeightMeta.weighedAt"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-40 mb-16">
|
||||
<UiNumberInput
|
||||
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"
|
||||
:min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<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>
|
||||
@@ -32,7 +41,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {ReceptionFormWeight} from '~/services/dto/reception-data'
|
||||
import { getReception } from '~/services/reception'
|
||||
import {getReception} from '~/services/reception'
|
||||
import {updateWeight} from "~/services/weight";
|
||||
import {useAuthStore} from "~/stores/auth";
|
||||
|
||||
@@ -45,17 +54,42 @@ const auth = useAuthStore()
|
||||
|
||||
const form = reactive({
|
||||
weights: [
|
||||
{ id: 0, type: 'tare' as const, weight: 0 },
|
||||
{ id: 0, type: 'gross' as const, weight: 0 }
|
||||
{id: 0, type: 'tare' as const, weight: 0, dsd: null, weighedAt: null},
|
||||
{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 tare = reception.weights.find(weight => weight.type === 'tare')
|
||||
const gross = reception.weights.find(weight => weight.type === 'gross')
|
||||
// On hydrate chaque ligne par son type (tare/gross), sans dépendre d'un index.
|
||||
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 }
|
||||
if (gross) form.weights[1] = { ...gross }
|
||||
// On récupère une valeur existante pour préremplir les champs partagés.
|
||||
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 () => {
|
||||
@@ -64,11 +98,23 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
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) {
|
||||
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>
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
/>
|
||||
<!-- Type d'expédition -->
|
||||
<div class="col-start-1 row-start-4">
|
||||
<label class="font-bold uppercase text-xl mb-2 block">
|
||||
<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-8 flex flex-row gap-6"
|
||||
class="mt-2 flex flex-row gap-6"
|
||||
>
|
||||
<UiNumberInput
|
||||
:label="type.label"
|
||||
@@ -344,7 +344,7 @@ watch(
|
||||
)
|
||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||
watch(
|
||||
() => [form.customerId, customers.value],
|
||||
() => [form.customerId, form.addressId, customers.value],
|
||||
() => {
|
||||
if (!form.customerId) {
|
||||
form.addressId = ''
|
||||
@@ -361,7 +361,11 @@ watch(
|
||||
(address) => String(address.id) === form.addressId
|
||||
)
|
||||
if (!matches) {
|
||||
form.addressId = ''
|
||||
if (customerAddresses.value.length === 1) {
|
||||
form.addressId = String(customerAddresses.value[0].id)
|
||||
} else {
|
||||
form.addressId = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<label
|
||||
v-if="label"
|
||||
:for="id"
|
||||
class="text-xl text-bold flex items-center"
|
||||
class="text-xl flex items-center"
|
||||
:class="labelClass"
|
||||
>
|
||||
<span
|
||||
@@ -25,7 +25,7 @@
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
v-bind="attrs"
|
||||
class="border-b border-black text-xl bg-transparent w-12"
|
||||
class="border-b border-black text-xl bg-transparent w-16"
|
||||
:class="[
|
||||
isEmpty ? 'text-neutral-400' : 'text-black',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-text',
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
"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."
|
||||
|
||||
@@ -24,34 +24,64 @@
|
||||
<aside class="bg-primary-500 text-white min-h-0 flex flex-col justify-between">
|
||||
<div class="flex flex-col gap-4 p-4 font-bold text-xl">
|
||||
<!-- Liste des liens à ajouter ci-dessous -->
|
||||
<NuxtLink to="/admin/dashboard">
|
||||
Tableau de bord
|
||||
<NuxtLink
|
||||
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 to="/admin/supplier/supplier-list">
|
||||
Fournisseur
|
||||
<NuxtLink
|
||||
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 to="/admin/carrier/carrier-list">
|
||||
Transporteur
|
||||
<NuxtLink
|
||||
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 to="/admin/user/list">
|
||||
Utilisateurs
|
||||
<NuxtLink to="/admin/user/list" custom v-slot="{ href, navigate }">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<p class="font-bold text-white text-left">v{{ version }}</p>
|
||||
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
<p class="font-bold text-white text-center pt-2">
|
||||
v{{ version }}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="min-h-0 overflow-auto px-12 py-12 ">
|
||||
<div class="w-full ">
|
||||
<slot />
|
||||
<slot/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -63,7 +93,9 @@
|
||||
import {useAuthStore} from '~/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const { version } = useAppVersion()
|
||||
const {version} = useAppVersion()
|
||||
|
||||
const route = useRoute()
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await auth.logout()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-white text-neutral-900">
|
||||
<header class="w-full border-b border-neutral-200 bg-primary-500">
|
||||
<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">
|
||||
<div class="flex w-full items-center justify-center px-6 py-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -21,12 +21,13 @@
|
||||
</a>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/admin/dashboard" custom v-slot="{ href, navigate, isActive }"
|
||||
to="/admin/dashboard" custom v-slot="{ href, navigate, isExactActive }"
|
||||
v-if="auth.isAdmin"
|
||||
>
|
||||
<a
|
||||
:href="href"
|
||||
@click="navigate"
|
||||
:class="isExactActive ? 'opacity-100' : 'opacity-50'"
|
||||
>
|
||||
Admin
|
||||
</a>
|
||||
@@ -100,7 +101,7 @@
|
||||
</aside>
|
||||
</transition>
|
||||
</header>
|
||||
<main class="mx-auto w-full max-w-[1280px] pb-0">
|
||||
<main class="mx-auto w-full max-w-[1280px]">
|
||||
<slot/>
|
||||
</main>
|
||||
<footer class="w-full mt-8 bg-primary-500 p-6">
|
||||
|
||||
192
frontend/pages/admin/customer/[[id]].vue
Normal file
192
frontend/pages/admin/customer/[[id]].vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<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-label" v-model="form.label" label="Nom du client" :disabled="!auth.isAdmin"/>
|
||||
<UiTextInput id="customer-code" v-model="form.code" label="Code" :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>({
|
||||
label: "",
|
||||
code: "",
|
||||
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.label = customer.label ?? ""
|
||||
form.code = customer.code ?? ""
|
||||
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 label = form.label.trim()
|
||||
const code = form.code.trim()
|
||||
|
||||
const customerPayload: CustomerPayload = {
|
||||
label,
|
||||
code,
|
||||
}
|
||||
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>
|
||||
112
frontend/pages/admin/customer/customer-list.vue
Normal file
112
frontend/pages/admin/customer/customer-list.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<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-7 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide"
|
||||
>
|
||||
<div>Nom</div>
|
||||
<div>Code</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 fournisseur.
|
||||
</div>
|
||||
|
||||
<div v-for="customer in customerList" :key="customer.id">
|
||||
<div
|
||||
v-if="!customer.addresses || customer.addresses.length === 0"
|
||||
class="grid grid-cols-7 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
|
||||
@click="goToCustomer(customer.id)"
|
||||
>
|
||||
<div class="truncate">{{ customer.label }}</div>
|
||||
<div class="truncate">{{ customer.code }}</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-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="goToCustomer(customer.id)"
|
||||
>
|
||||
<div class="truncate">
|
||||
{{ idx === 0 ? customer.label : "↳" }}
|
||||
</div>
|
||||
<div class="truncate">{{ idx === 0 ? customer.code : "" }}</div>
|
||||
<div class="truncate">{{ address.street || "—" }}</div>
|
||||
<div class="truncate">{{ address.street2 || "—" }}</div>
|
||||
<div>{{ address.postalCode || "—" }}</div>
|
||||
<div class="uppercase truncate">{{ address.city || "—" }}</div>
|
||||
<div class="uppercase truncate">{{ address.countryCode || "—" }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
class="grid grid-cols-7 hover:bg-slate-50 border-t gap-4 px-4 py-2 cursor-pointer"
|
||||
@click="goToCustomer(customer.id)"
|
||||
>
|
||||
<div class="truncate">{{ customer.label }}</div>
|
||||
<div class="truncate">{{ customer.code }}</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,6 +1,6 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
<div class="flex items-center justify-between gap-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold uppercase">
|
||||
{{ supplierId ? "Modifications du fournisseur" : "Ajout d'un fournisseur" }}
|
||||
</h1>
|
||||
@@ -14,13 +14,13 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-y-16 gap-x-12 mb-10 py-12 border-b border-black ">
|
||||
<div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12">
|
||||
<UiTextInput id="supplier-name" v-model="form.name" label="Nom du fournisseur" :disabled="!auth.isAdmin"/>
|
||||
<UiTextInput id="supplier-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="flex items-center justify-between mb-4 py-6 border-t border-black"></div>
|
||||
<div class="mx-24 mb-4 py-6 border-t border-black"></div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-3xl font-bold uppercase">Adresses fournisseur</h2>
|
||||
<button
|
||||
@@ -179,14 +179,17 @@ async function validate() {
|
||||
email,
|
||||
phone,
|
||||
}
|
||||
let targetId: number | null = null
|
||||
|
||||
if (supplierId.value !== null) {
|
||||
await updateSupplier(supplierId.value, supplierPayload)
|
||||
targetId = supplierId.value
|
||||
} else {
|
||||
await createSupplier(supplierPayload)
|
||||
const created = await createSupplier(supplierPayload)
|
||||
targetId = created.id
|
||||
}
|
||||
|
||||
await router.push("/admin/supplier/supplier-list")
|
||||
await router.push(`/admin/supplier/${targetId}`)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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"
|
||||
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
|
||||
@@ -45,7 +45,7 @@ definePageMeta({
|
||||
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'
|
||||
import type {UserData, UserFormData, UserPayload} from '~/services/dto/user-data'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -105,10 +105,12 @@ async function validate() {
|
||||
const normalizedRole = form.role.trim()
|
||||
const normalizedPassword = form.password.trim()
|
||||
|
||||
const basePayload = {
|
||||
const basePayload: UserPayload = {
|
||||
username: normalizedUsername,
|
||||
roles: normalizedRole ? [normalizedRole] : undefined,
|
||||
password: normalizedPassword || undefined
|
||||
}
|
||||
if (normalizedPassword) {
|
||||
basePayload.password = normalizedPassword
|
||||
}
|
||||
|
||||
if (userId.value) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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">
|
||||
<UiStepper
|
||||
:labels="RECEPTION_STEP_LABELS"
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<template>
|
||||
|
||||
<form @submit.prevent="validate">
|
||||
<div class="flex items-center justify-between mt-8 mb-8 ">
|
||||
<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 class="flex items-center justify-between mt-12 mb-8 ">
|
||||
<h1 class="font-bold text-5xl uppercase">Réception {{ receptionLoad?.identificationNumber }}</h1>
|
||||
</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 -->
|
||||
<UiSelect
|
||||
id="reception-user"
|
||||
@@ -120,28 +114,50 @@
|
||||
wrapper-class="col-start-2 row-start-4"
|
||||
/>
|
||||
</div>
|
||||
<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" @click="isBtWeight = true" >pesées</h1>
|
||||
<h1 class="font-bold text-5xl uppercase col-start-2 row-start-1" @click="isBtWeight = false">{{isMerchandise ? "Marchandises" : "Bovins"}}</h1>
|
||||
<div class="flex justify-center mb-2">
|
||||
<button
|
||||
v-if="auth.isAdmin"
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] mb-16"
|
||||
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
<update-weight
|
||||
v-if="isBtWeight"
|
||||
:idReception="idReception"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
<div class="flex justify-evenly gap-y-8 gap-x-40 mb-8 border-b border-slate-400">
|
||||
<h1
|
||||
class="font-bold text-3xl uppercase col-start-1 row-start-1 cursor-pointer"
|
||||
: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
|
||||
v-else-if="isMerchandise"
|
||||
:idReception="idReception"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
<update-weight
|
||||
v-if="activeTab === 'weights'"
|
||||
:idReception="idReception"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
|
||||
<update-bovin
|
||||
v-else
|
||||
:idReception="idReception"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
<update-merchandise
|
||||
v-else-if="activeTab === 'merchandise' && isMerchandise"
|
||||
:idReception="idReception"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
|
||||
<update-bovin
|
||||
v-else
|
||||
:idReception="idReception"
|
||||
:disabled="!auth.isAdmin"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -168,6 +184,7 @@ import UpdateWeight from "~/components/reception/update-weight.vue";
|
||||
import UpdateMerchandise from "~/components/reception/update-merchandise.vue";
|
||||
import UpdateBovin from "~/components/reception/update-bovin.vue";
|
||||
|
||||
const activeTab = ref<'weights' | 'merchandise'>('weights')
|
||||
const router = useRouter()
|
||||
const receptionStore = useReceptionStore()
|
||||
const form = reactive<ReceptionFormData>({
|
||||
@@ -249,7 +266,7 @@ const clearReceptionBovines = async (receptionIri: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const hydrateFromUser = (reception: ReceptionData | null)=> {
|
||||
const hydrateFromUser = (reception: ReceptionData | null) => {
|
||||
if (!reception) {
|
||||
return
|
||||
}
|
||||
@@ -378,7 +395,7 @@ onMounted(async () => {
|
||||
|
||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||
watch(
|
||||
() => [form.supplierId, suppliers.value],
|
||||
() => [form.supplierId, form.addressId, suppliers.value],
|
||||
() => {
|
||||
if (!form.supplierId) {
|
||||
form.addressId = ''
|
||||
@@ -395,7 +412,11 @@ watch(
|
||||
(address) => String(address.id) === form.addressId
|
||||
)
|
||||
if (!matches) {
|
||||
form.addressId = ''
|
||||
if (supplierAddresses.value.length === 1) {
|
||||
form.addressId = String(supplierAddresses.value[0].id)
|
||||
} else {
|
||||
form.addressId = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
@@ -532,7 +553,7 @@ async function validate() {
|
||||
}
|
||||
|
||||
if (idReception) {
|
||||
const updated = await receptionStore.updateReception(idReception,{
|
||||
const updated = await receptionStore.updateReception(idReception, {
|
||||
...payload
|
||||
})
|
||||
if (updated) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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">
|
||||
<UiStepper
|
||||
:labels="SHIPMENT_STEP_LABELS"
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { CustomerData } from '~/services/dto/customer-data'
|
||||
import { useApi } from "~/composables/useApi"
|
||||
import type { CustomerData, CustomerPayload } from "~/services/dto/customer-data"
|
||||
|
||||
export type CustomerListResponse =
|
||||
| CustomerData[]
|
||||
| { 'hydra:member'?: CustomerData[] }
|
||||
| { "hydra:member"?: CustomerData[] }
|
||||
|
||||
export async function getCustomerList(): Promise<CustomerData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<CustomerListResponse>('customers', {}, {
|
||||
toastErrorKey: 'errors.customer.list'
|
||||
const response = await api.get<CustomerListResponse>("customers", {}, {
|
||||
toastErrorKey: "errors.customer.list",
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
if (Array.isArray(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 []
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import type { AddressData } from "~/services/dto/address-data"
|
||||
import type { AddressFormData } from "~/services/dto/address-data"
|
||||
|
||||
export type CustomerAddresses = AddressFormData[] | string[]
|
||||
|
||||
export interface CustomerData {
|
||||
id: number
|
||||
label: string
|
||||
code?: string | null
|
||||
addresses?: AddressData[] | null
|
||||
addresses: CustomerAddresses
|
||||
}
|
||||
|
||||
export interface CustomerFormData {
|
||||
label: string
|
||||
code?: string
|
||||
addresses: AddressFormData[]
|
||||
}
|
||||
|
||||
export type CustomerPayload = {
|
||||
label: string
|
||||
code?: string | null
|
||||
addresses?: string[]
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ export interface WeightData {
|
||||
weight: number | null
|
||||
dsd: number | null
|
||||
weighedAt: string | null
|
||||
type : string | null
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
normalizationContext: ['groups' => ['carrier:read']],
|
||||
security: "is_granted('ROLE_USER')"
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['carrier:read']],
|
||||
security: "is_granted('ROLE_USER')"
|
||||
),
|
||||
new Post(
|
||||
normalizationContext: ['groups' => ['carrier:read']],
|
||||
|
||||
@@ -8,6 +8,8 @@ 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;
|
||||
@@ -24,6 +26,16 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
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')",
|
||||
)]
|
||||
@@ -36,11 +48,11 @@ class Customer
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['customer:read', 'shipment:read'])]
|
||||
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['customer:read', 'shipment:read'])]
|
||||
#[Groups(['customer:read', 'customer:write', 'shipment:read'])]
|
||||
private ?string $code = null;
|
||||
|
||||
/**
|
||||
@@ -48,7 +60,7 @@ class Customer
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'customers')]
|
||||
#[ORM\JoinTable(name: 'customer_address')]
|
||||
#[Groups(['customer:read'])]
|
||||
#[Groups(['customer:read', 'customer:write'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private Collection $addresses;
|
||||
|
||||
@@ -87,8 +99,29 @@ class Customer
|
||||
return $this->addresses;
|
||||
}
|
||||
|
||||
public function setAddresses(Collection $addresses): void
|
||||
public function setAddresses(iterable $addresses): self
|
||||
{
|
||||
$this->addresses = $addresses;
|
||||
$this->addresses->clear();
|
||||
foreach ($addresses as $address) {
|
||||
$this->addAddress($address);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addAddress(Address $address): self
|
||||
{
|
||||
if (!$this->addresses->contains($address)) {
|
||||
$this->addresses->add($address);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeAddress(Address $address): self
|
||||
{
|
||||
$this->addresses->removeElement($address);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,10 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
normalizationContext: ['groups' => ['supplier:read']],
|
||||
security: "is_granted('ROLE_USER')"
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['supplier:read']],
|
||||
),
|
||||
new GetCollection(
|
||||
uriTemplate: '/admin/suppliers',
|
||||
normalizationContext: ['groups' => ['supplier:read']],
|
||||
security: "is_granted('ROLE_ADMIN')"
|
||||
),
|
||||
new Post(
|
||||
|
||||
@@ -21,8 +21,12 @@ final class UserPasswordProcessor implements ProcessorInterface
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
{
|
||||
if ($data instanceof User) {
|
||||
$plain = $data->getPassword();
|
||||
if ('' !== $plain) {
|
||||
$plain = $data->getPassword();
|
||||
$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,
|
||||
$plain
|
||||
|
||||
Reference in New Issue
Block a user