Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
696100a622 | ||
| 97f21ab35c | |||
|
|
fa7b44fb02 | ||
| 9be2e0c379 | |||
|
|
fee7bbb2ec | ||
| b707aae0e8 | |||
|
|
d0beb80199 | ||
| c378b402c4 | |||
|
|
6e707484a0 | ||
| 0067e51e6e | |||
|
|
1c0cdeb085 | ||
| 465339cdd6 | |||
|
|
2bc484574f | ||
| ea1e3b074c |
@@ -2,7 +2,9 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm run:*)",
|
||||
"WebFetch(domain:geo.api.gouv.fr)"
|
||||
"WebFetch(domain:geo.api.gouv.fr)",
|
||||
"Bash(pip3 install:*)",
|
||||
"Bash(python3 -c \":*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
run: |
|
||||
cd frontend
|
||||
npm ci
|
||||
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ npm run generate
|
||||
CI=1 NUXT_TELEMETRY_DISABLED=1 NUXT_PUBLIC_API_BASE=/api NUXT_PUBLIC_APP_BASE=/ NUXT_PUBLIC_GEO_API_BASE=https://geo.api.gouv.fr npm run generate
|
||||
test -f .output/public/index.html
|
||||
|
||||
- name: Build artefact
|
||||
|
||||
208
.idea/workspace.xml
generated
208
.idea/workspace.xml
generated
@@ -4,11 +4,19 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : correction des retours de la V0">
|
||||
<change beforePath="$PROJECT_DIR$/.claude/settings.local.json" beforeDir="false" afterPath="$PROJECT_DIR$/.claude/settings.local.json" afterDir="false" />
|
||||
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : système de blocage utilisateur">
|
||||
<change afterPath="$PROJECT_DIR$/migrations/Version20260325142815.php" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/Security/UserChecker.php" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/State/ActiveUsersProvider.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/components/address.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/address.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/nuxt.config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/nuxt.config.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/packages/security.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/config/packages/security.yaml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/admin/user/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/user/[[id]].vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/pages/admin/user/list.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/admin/user/list.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/services/dto/user-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/user-data.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/frontend/stores/auth.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/stores/auth.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Entity/User.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/User.php" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -40,7 +48,7 @@
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="feat/327-voir-modifier-une-expedition-terminee" />
|
||||
<entry key="$PROJECT_DIR$" value="fix/FER-11-corriger-le-probleme-de-bearer-token" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
@@ -224,36 +232,36 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<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": "develop",
|
||||
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/m-tristan/workspace/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": "advanced.settings",
|
||||
"ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<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": "feature/FER-12-ajouter-un-blocage-des-utilisateurs",
|
||||
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/m-tristan/workspace/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": "advanced.settings",
|
||||
"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\m-tristan\workspace\Ferme" />
|
||||
@@ -263,8 +271,8 @@
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\kevin\Stage\Ferme\frontend\components\shipment" />
|
||||
</key>
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="C:\Users\m-tristan\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\m-tristan\workspace\Ferme" />
|
||||
<recent name="C:\Users\m-tristan\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||
<recent name="\\wsl.localhost\Ubuntu-24.04\home\tristan\workspace\ferme\templates" />
|
||||
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches" />
|
||||
<recent name="C:\Users\autin\AppData\Roaming\JetBrains\PhpStorm2025.3\scratches\Ferme_MCD\MCD_DOC" />
|
||||
@@ -273,7 +281,7 @@
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.31033.138" />
|
||||
<option value="bundled-php-predefined-a98d8de5180a-0e0d91225499-com.jetbrains.php.sharedIndexes-PS-253.32098.40" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
@@ -323,63 +331,9 @@
|
||||
<workItem from="1773215356754" duration="5754000" />
|
||||
<workItem from="1773756072697" duration="5450000" />
|
||||
<workItem from="1773766075191" duration="6202000" />
|
||||
<workItem from="1773824491213" duration="15167000" />
|
||||
</task>
|
||||
<task id="LOCAL-00025" summary="fix : modification du script de déploiement pour corriger le problème d'écriture des logs de prod">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769079030808</created>
|
||||
<option name="number" value="00025" />
|
||||
<option name="presentableId" value="LOCAL-00025" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769079030808</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00026" summary="fix : doc de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769094376813</created>
|
||||
<option name="number" value="00026" />
|
||||
<option name="presentableId" value="LOCAL-00026" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769094376813</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00027" summary="fix : doc et script de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769096187792</created>
|
||||
<option name="number" value="00027" />
|
||||
<option name="presentableId" value="LOCAL-00027" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769096187792</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00028" summary="fix : doc et script de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769097091268</created>
|
||||
<option name="number" value="00028" />
|
||||
<option name="presentableId" value="LOCAL-00028" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769097091268</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00029" summary="fix : gitea workflow">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769097476629</created>
|
||||
<option name="number" value="00029" />
|
||||
<option name="presentableId" value="LOCAL-00029" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769097476629</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00030" summary="fix : script de déploiement">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769098182184</created>
|
||||
<option name="number" value="00030" />
|
||||
<option name="presentableId" value="LOCAL-00030" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769098182184</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00031" summary="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil">
|
||||
<option name="closed" value="true" />
|
||||
<created>1769098861988</created>
|
||||
<option name="number" value="00031" />
|
||||
<option name="presentableId" value="LOCAL-00031" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1769098861988</updated>
|
||||
<workItem from="1773824491213" duration="24805000" />
|
||||
<workItem from="1774275549972" duration="51000" />
|
||||
<workItem from="1774276665015" duration="16178000" />
|
||||
</task>
|
||||
<task id="LOCAL-00032" summary="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster">
|
||||
<option name="closed" value="true" />
|
||||
@@ -717,7 +671,63 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1773841634554</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="74" />
|
||||
<task id="LOCAL-00074" summary="feat : ajout de l'api de l'état pour chercher les villes via le CP">
|
||||
<option name="closed" value="true" />
|
||||
<created>1773842791819</created>
|
||||
<option name="number" value="00074" />
|
||||
<option name="presentableId" value="LOCAL-00074" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1773842791819</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00075" summary="fix : script de déploiement + CI/CD build de l'app">
|
||||
<option name="closed" value="true" />
|
||||
<created>1773843922376</created>
|
||||
<option name="number" value="00075" />
|
||||
<option name="presentableId" value="LOCAL-00075" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1773843922377</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00076" summary="fix : order navbar + modification création fournisseur et client">
|
||||
<option name="closed" value="true" />
|
||||
<created>1773852806120</created>
|
||||
<option name="number" value="00076" />
|
||||
<option name="presentableId" value="LOCAL-00076" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1773852806121</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00077" summary="fix : order récéption/expédition + correction style bouton récéption">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774283204849</created>
|
||||
<option name="number" value="00077" />
|
||||
<option name="presentableId" value="LOCAL-00077" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774283204849</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00078" summary="fix : style bon de récéption">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774285464091</created>
|
||||
<option name="number" value="00078" />
|
||||
<option name="presentableId" value="LOCAL-00078" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774285464091</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00079" summary="fix : bouton de mise en attente">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774337609427</created>
|
||||
<option name="number" value="00079" />
|
||||
<option name="presentableId" value="LOCAL-00079" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774337609427</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00080" summary="fix : problème de bearer token">
|
||||
<option name="closed" value="true" />
|
||||
<created>1774448105945</created>
|
||||
<option name="number" value="00080" />
|
||||
<option name="presentableId" value="LOCAL-00080" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1774448105945</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="81" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -767,14 +777,6 @@
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address et modification du numéro de réception" />
|
||||
<MESSAGE value="feat : ajout de colonne pour les Supplier, Address. Modification du numéro de réception et ajout de fixtures" />
|
||||
<MESSAGE value="feat : mise à jour du bon de réception" />
|
||||
<MESSAGE value="feat : Ajout de la sélection des bovins étape 3 d'une réception (WIP)" />
|
||||
<MESSAGE value="feat : 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" />
|
||||
<MESSAGE value="fix : corrections frontend" />
|
||||
@@ -792,7 +794,15 @@
|
||||
<MESSAGE value="feat : ajout de bâtiment dans les fixtures et seed + organisation du menu" />
|
||||
<MESSAGE value="fix : on ne pèse plus automatiquement + fix message de création réception" />
|
||||
<MESSAGE value="fix : correction des retours de la V0" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix : correction des retours de la V0" />
|
||||
<MESSAGE value="feat : ajout de l'api de l'état pour chercher les villes via le CP" />
|
||||
<MESSAGE value="fix : script de déploiement + CI/CD build de l'app" />
|
||||
<MESSAGE value="fix : order navbar + modification création fournisseur et client" />
|
||||
<MESSAGE value="fix : order récéption/expédition + correction style bouton récéption" />
|
||||
<MESSAGE value="fix : style bon de récéption" />
|
||||
<MESSAGE value="fix : bouton de mise en attente" />
|
||||
<MESSAGE value="fix : problème de bearer token" />
|
||||
<MESSAGE value="feat : système de blocage utilisateur" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="feat : système de blocage utilisateur" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
|
||||
@@ -59,6 +59,9 @@ Ajouter dans le fichier .env du frontend
|
||||
* [#356] front page admin bovin
|
||||
* [#353] modification front admin client
|
||||
* [#353] modification front admin utilisateur
|
||||
* [#FER-11] Corriger le problème de bearer token
|
||||
* [#FER-12] Ajouter un blocage des utilisateurs
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -20,6 +20,7 @@ security:
|
||||
pattern: ^/login_check
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
user_checker: App\Security\UserChecker
|
||||
json_login:
|
||||
check_path: /login_check
|
||||
username_path: username
|
||||
@@ -30,6 +31,7 @@ security:
|
||||
pattern: ^/
|
||||
stateless: true
|
||||
provider: app_user_provider
|
||||
user_checker: App\Security\UserChecker
|
||||
jwt: ~
|
||||
logout:
|
||||
path: /api/logout
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.0.72'
|
||||
app.version: '0.0.79'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<form :class="{ submitted }" @submit.prevent="validate">
|
||||
<form ref="formRef" :class="{ submitted }" @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 text-primary-500">Réception</h1>
|
||||
<UiSelect
|
||||
@@ -116,7 +116,7 @@
|
||||
<div class="flex justify-center">
|
||||
<UiButton
|
||||
type="submit"
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||
class="text-xl mb-16 uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
|
||||
@click="submitted = true"
|
||||
>Valider
|
||||
</UiButton>
|
||||
@@ -141,6 +141,7 @@ const router = useRouter()
|
||||
const receptionStore = useReceptionStore()
|
||||
const isHydrating = ref(false)
|
||||
const submitted = ref(false)
|
||||
const formRef = ref<HTMLFormElement | null>(null)
|
||||
|
||||
const form = reactive<ReceptionFormData>({
|
||||
licensePlate: '',
|
||||
@@ -217,7 +218,7 @@ onMounted(async () => {
|
||||
await loadVehicles()
|
||||
})
|
||||
|
||||
async function validate() {
|
||||
const buildPayload = () => {
|
||||
const normalizedLicensePlate = form.licensePlate.trim()
|
||||
const normalizedReceptionDate = form.receptionDate.trim()
|
||||
const normalizedReceptionTypeId = form.receptionTypeId.trim()
|
||||
@@ -236,7 +237,7 @@ async function validate() {
|
||||
const carrierIri = normalizedCarrierId ? `/api/carriers/${normalizedCarrierId}` : null
|
||||
const driverIri = normalizedDriverId ? `/api/drivers/${normalizedDriverId}` : null
|
||||
|
||||
const basePayload = {
|
||||
return {
|
||||
licensePlate: normalizedLicensePlate,
|
||||
receptionDate: normalizedReceptionDate,
|
||||
receptionType: receptionTypeIri,
|
||||
@@ -244,13 +245,35 @@ async function validate() {
|
||||
supplier: supplierIri,
|
||||
address: addressIri,
|
||||
truck: truckIri,
|
||||
carrier: carrierIri
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...basePayload,
|
||||
carrier: carrierIri,
|
||||
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {})
|
||||
}
|
||||
}
|
||||
|
||||
const saveDraft = async () => {
|
||||
const payload = buildPayload()
|
||||
if (!receptionStore.current) {
|
||||
await receptionStore.createReception({
|
||||
currentStep: 0,
|
||||
...payload
|
||||
})
|
||||
return
|
||||
}
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
currentStep: receptionStore.current.currentStep,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
|
||||
const validateFields = () => {
|
||||
submitted.value = true
|
||||
return formRef.value?.reportValidity() ?? false
|
||||
}
|
||||
|
||||
defineExpose({ saveDraft, validateFields })
|
||||
|
||||
async function validate() {
|
||||
const payload = buildPayload()
|
||||
|
||||
if (!receptionStore.current) {
|
||||
const created = await receptionStore.createReception({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<form :class="{ submitted }" @submit.prevent="validate">
|
||||
<form ref="formRef" :class="{ submitted }" @submit.prevent="validate">
|
||||
<div class="grid grid-cols-2 h-[461px] items-start gap-y-8 gap-x-40 mb-16">
|
||||
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Expédition</h1>
|
||||
<UiSelect
|
||||
@@ -152,6 +152,7 @@ const router = useRouter()
|
||||
const shipmentStore = useShipmentStore()
|
||||
const isHydrating = ref(false)
|
||||
const submitted = ref(false)
|
||||
const formRef = ref<HTMLFormElement | null>(null)
|
||||
|
||||
const form = reactive<ShipmentFormData>({
|
||||
userId: '',
|
||||
@@ -286,7 +287,12 @@ const saveDraft = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ saveDraft })
|
||||
const validateFields = () => {
|
||||
submitted.value = true
|
||||
return formRef.value?.reportValidity() ?? false
|
||||
}
|
||||
|
||||
defineExpose({ saveDraft, validateFields })
|
||||
|
||||
const validate = async () => {
|
||||
const payload = buildPayload()
|
||||
|
||||
@@ -60,6 +60,7 @@ const {
|
||||
title,
|
||||
fetchWeight,
|
||||
saveWeight,
|
||||
saveWeightDraft,
|
||||
showGenerateReceipt,
|
||||
printReceipt
|
||||
} = useWeighingStep({
|
||||
@@ -75,4 +76,6 @@ const {
|
||||
clearEntity: props.clearEntity,
|
||||
buildReceiptFilename: props.buildReceiptFilename
|
||||
})
|
||||
|
||||
defineExpose({ saveWeightDraft })
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,8 @@ export const useWeighingStep = (options: UseWeighingStepOptions) => {
|
||||
title,
|
||||
showLoadingBox,
|
||||
fetchWeight,
|
||||
saveWeight
|
||||
saveWeight,
|
||||
saveWeightDraft
|
||||
} = useWeighing({
|
||||
mode: options.mode,
|
||||
entity: options.entity,
|
||||
@@ -71,6 +72,7 @@ export const useWeighingStep = (options: UseWeighingStepOptions) => {
|
||||
showLoadingBox,
|
||||
fetchWeight,
|
||||
saveWeight,
|
||||
saveWeightDraft,
|
||||
showGenerateReceipt,
|
||||
printReceipt
|
||||
}
|
||||
|
||||
@@ -90,6 +90,38 @@ export const useWeighing = ({
|
||||
}
|
||||
}
|
||||
|
||||
const saveWeightDraft = async () => {
|
||||
if (!entity.value) return
|
||||
if (!weightData.value && !currentWeightEntry.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
|
||||
|
||||
const relationPayload: Record<string, string> = {}
|
||||
relationPayload[entityName] = `/api/${apiResource}/${entity.value.id}`
|
||||
|
||||
if (existingEntry?.id) {
|
||||
await updateWeight(existingEntry.id, {
|
||||
type: mode,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
weighedAt: baseWeighedAt
|
||||
})
|
||||
} else {
|
||||
await createWeight({
|
||||
...relationPayload,
|
||||
type: mode,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
weighedAt: baseWeighedAt
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
weightData,
|
||||
currentWeightEntry,
|
||||
@@ -98,7 +130,8 @@ export const useWeighing = ({
|
||||
title,
|
||||
showLoadingBox,
|
||||
fetchWeight,
|
||||
saveWeight
|
||||
saveWeight,
|
||||
saveWeightDraft
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ export const useWorkflowSteps = (config: WorkflowConfig, store: WorkflowStore) =
|
||||
return
|
||||
}
|
||||
const datePayload: Record<string, any> = {}
|
||||
datePayload[config.dateField] = store.current[config.dateField]
|
||||
const rawDate = store.current[config.dateField]
|
||||
datePayload[config.dateField] = rawDate ? rawDate.slice(0, 10) : rawDate
|
||||
await store[updateMethod](store.current.id, {
|
||||
currentStep: store.current.currentStep,
|
||||
licensePlate: store.current.licensePlate,
|
||||
|
||||
@@ -72,23 +72,6 @@
|
||||
</a>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="auth.isAdmin"
|
||||
to="/admin/carrier/carrier-list"
|
||||
custom
|
||||
v-slot="{ href, navigate }"
|
||||
>
|
||||
<a
|
||||
:href="href"
|
||||
@click="navigate"
|
||||
:class="route.path.startsWith('/admin/carrier')
|
||||
? 'opacity-100'
|
||||
: 'opacity-65 hover:opacity-100 transition'"
|
||||
>
|
||||
Transporteurs
|
||||
</a>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="auth.isAdmin"
|
||||
to="/admin/customer/customer-list"
|
||||
@@ -106,6 +89,23 @@
|
||||
</a>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="auth.isAdmin"
|
||||
to="/admin/carrier/carrier-list"
|
||||
custom
|
||||
v-slot="{ href, navigate }"
|
||||
>
|
||||
<a
|
||||
:href="href"
|
||||
@click="navigate"
|
||||
:class="route.path.startsWith('/admin/carrier')
|
||||
? 'opacity-100'
|
||||
: 'opacity-65 hover:opacity-100 transition'"
|
||||
>
|
||||
Transporteurs
|
||||
</a>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
v-if="auth.isAdmin"
|
||||
to="/admin/bovin/bovin-list"
|
||||
|
||||
@@ -14,6 +14,19 @@
|
||||
<UiTextInput id="customer-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||
<UiTextInput id="customer-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||
</div>
|
||||
<div v-if="!customerId" class="flex flex-cols-3 justify-between mb-11">
|
||||
<UiTextInput id="address-street" v-model="addressForm.street" label="Rue" wrapper-class="w-[280px]" required />
|
||||
<UiTextInput id="address-street2" v-model="addressForm.street2" label="Complément" wrapper-class="w-[280px]" />
|
||||
<UiTextInput id="address-country" v-model="addressForm.countryCode" label="Pays (code)" wrapper-class="w-[280px]" />
|
||||
</div>
|
||||
<div v-if="!customerId" class="flex flex-cols-3 justify-between mb-11">
|
||||
<UiTextInput id="address-postalCode" v-model="addressForm.postalCode" label="Code postal" wrapper-class="w-[280px]" required />
|
||||
<UiSelect id="address-city" v-model="addressForm.city" label="Ville"
|
||||
:options="communeOptions" :loading="isLoadingCities"
|
||||
wrapper-class="w-[280px]" required />
|
||||
<div class="w-[280px]" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<UiButton
|
||||
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||
@@ -26,6 +39,7 @@
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<template v-if="customerId">
|
||||
<div class="flex items-center justify-between mb-7">
|
||||
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du client</h2>
|
||||
</div>
|
||||
@@ -77,6 +91,7 @@
|
||||
Ajouter
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -84,6 +99,8 @@
|
||||
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 {createAddress, type AddressPayload} from "~/services/address"
|
||||
import {getCommunesByPostalCode, type CommuneData} from "~/services/geo"
|
||||
import {useAuthStore} from "~/stores/auth"
|
||||
|
||||
const route = useRoute()
|
||||
@@ -106,6 +123,30 @@ const form = reactive<CustomerFormData>({
|
||||
addresses: [],
|
||||
})
|
||||
|
||||
// Address form (creation mode only)
|
||||
const addressForm = reactive<AddressPayload>({
|
||||
street: "", street2: null, postalCode: "", city: "", countryCode: "FR",
|
||||
})
|
||||
const communes = ref<CommuneData[]>([])
|
||||
const isLoadingCities = ref(false)
|
||||
const communeOptions = computed(() => communes.value.map(c => ({ value: c.nom, label: c.nom })))
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
watch(() => addressForm.postalCode, (cp) => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
if (!cp || cp.length < 5) { communes.value = []; addressForm.city = ''; return }
|
||||
if (cp.length === 5) {
|
||||
debounceTimer = setTimeout(async () => {
|
||||
isLoadingCities.value = true
|
||||
try {
|
||||
communes.value = await getCommunesByPostalCode(cp)
|
||||
if (communes.value.length === 1) addressForm.city = communes.value[0].nom
|
||||
else addressForm.city = ''
|
||||
} finally { isLoadingCities.value = false }
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
const goToAddAddress = () => {
|
||||
if (customerId.value === null || !auth.isAdmin) return
|
||||
router.push({
|
||||
@@ -187,8 +228,11 @@ async function validate() {
|
||||
await updateCustomer(customerId.value, customerPayload)
|
||||
targetId = customerId.value
|
||||
} else {
|
||||
const addressData = await createAddress({ ...addressForm })
|
||||
const addressIRI = `/api/addresses/${addressData.id}`
|
||||
const creationPayload = {
|
||||
...customerPayload,
|
||||
addresses: [addressIRI],
|
||||
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
||||
}
|
||||
const created = await createCustomer(creationPayload)
|
||||
|
||||
@@ -15,6 +15,19 @@
|
||||
<UiTextInput id="supplier-phone" v-model="form.phone" label="Téléphone" :disabled="!auth.isAdmin" wrapper-class="w-[280px]" required/>
|
||||
<UiTextInput id="supplier-email" v-model="form.email" label="Email" :disabled="!auth.isAdmin" wrapper-class="w-[280px]"/>
|
||||
</div>
|
||||
<div v-if="!supplierId" class="flex flex-cols-3 justify-between mb-11">
|
||||
<UiTextInput id="address-street" v-model="addressForm.street" label="Rue" wrapper-class="w-[280px]" required />
|
||||
<UiTextInput id="address-street2" v-model="addressForm.street2" label="Complément" wrapper-class="w-[280px]" />
|
||||
<UiTextInput id="address-country" v-model="addressForm.countryCode" label="Pays (code)" wrapper-class="w-[280px]" />
|
||||
</div>
|
||||
<div v-if="!supplierId" class="flex flex-cols-3 justify-between mb-11">
|
||||
<UiTextInput id="address-postalCode" v-model="addressForm.postalCode" label="Code postal" wrapper-class="w-[280px]" required />
|
||||
<UiSelect id="address-city" v-model="addressForm.city" label="Ville"
|
||||
:options="communeOptions" :loading="isLoadingCities"
|
||||
wrapper-class="w-[280px]" required />
|
||||
<div class="w-[280px]" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<UiButton
|
||||
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||
@@ -27,6 +40,7 @@
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<template v-if="supplierId">
|
||||
<div class="flex items-center justify-between mb-7">
|
||||
<h2 class="text-3xl text-primary-500 font-bold uppercase">Adresses du fournisseur</h2>
|
||||
</div>
|
||||
@@ -78,6 +92,7 @@
|
||||
Ajouter
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -85,6 +100,8 @@
|
||||
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 {createAddress, type AddressPayload} from "~/services/address"
|
||||
import {getCommunesByPostalCode, type CommuneData} from "~/services/geo"
|
||||
import {useAuthStore} from "~/stores/auth"
|
||||
|
||||
const route = useRoute()
|
||||
@@ -107,6 +124,30 @@ const form = reactive<SupplierFormData>({
|
||||
addresses: [],
|
||||
})
|
||||
|
||||
// Address form (creation mode only)
|
||||
const addressForm = reactive<AddressPayload>({
|
||||
street: "", street2: null, postalCode: "", city: "", countryCode: "FR",
|
||||
})
|
||||
const communes = ref<CommuneData[]>([])
|
||||
const isLoadingCities = ref(false)
|
||||
const communeOptions = computed(() => communes.value.map(c => ({ value: c.nom, label: c.nom })))
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
watch(() => addressForm.postalCode, (cp) => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
if (!cp || cp.length < 5) { communes.value = []; addressForm.city = ''; return }
|
||||
if (cp.length === 5) {
|
||||
debounceTimer = setTimeout(async () => {
|
||||
isLoadingCities.value = true
|
||||
try {
|
||||
communes.value = await getCommunesByPostalCode(cp)
|
||||
if (communes.value.length === 1) addressForm.city = communes.value[0].nom
|
||||
else addressForm.city = ''
|
||||
} finally { isLoadingCities.value = false }
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
const goToAddAddress = () => {
|
||||
if (supplierId.value === null || !auth.isAdmin) return
|
||||
router.push({
|
||||
@@ -190,8 +231,11 @@ async function validate() {
|
||||
await updateSupplier(supplierId.value, supplierPayload)
|
||||
targetId = supplierId.value
|
||||
} else {
|
||||
const addressData = await createAddress({ ...addressForm })
|
||||
const addressIRI = `/api/addresses/${addressData.id}`
|
||||
const creationPayload = {
|
||||
...supplierPayload,
|
||||
addresses: [addressIRI],
|
||||
...(auth.user?.id ? { createdBy: `/api/users/${auth.user.id}` } : {}),
|
||||
}
|
||||
const created = await createSupplier(creationPayload)
|
||||
|
||||
@@ -41,10 +41,24 @@
|
||||
type="password"
|
||||
:disabled="!auth.isAdmin"
|
||||
wrapper-class="w-[280px]"
|
||||
required
|
||||
:required="!userId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-11">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
id="user-locked"
|
||||
v-model="form.isLocked"
|
||||
type="checkbox"
|
||||
:disabled="!auth.isAdmin"
|
||||
class="w-5 h-5 accent-primary-500"
|
||||
/>
|
||||
<span class="text-sm text-primary-700">Verrouiller le compte</span>
|
||||
</label>
|
||||
<p class="ml-4 text-xs text-slate-400">Un compte verrouillé ne peut plus se connecter.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<UiButton
|
||||
class="inline-flex mb-28 items-center justify-center text-xl min-w-[194px] text-white uppercase bg-primary-500 h-[50px] rounded hover:opacity-80 justify-self-end"
|
||||
@@ -86,7 +100,8 @@ const resolveUserId = (param: unknown) => {
|
||||
const form = reactive<UserFormData>({
|
||||
username: '',
|
||||
password: '',
|
||||
role: ''
|
||||
role: '',
|
||||
isLocked: false
|
||||
})
|
||||
|
||||
const hydrateFromUser = (user: UserData | null) => {
|
||||
@@ -99,6 +114,7 @@ const hydrateFromUser = (user: UserData | null) => {
|
||||
const hasAdmin = roles.includes('ROLE_ADMIN')
|
||||
form.role = hasAdmin ? 'ROLE_ADMIN' : 'ROLE_USER'
|
||||
form.password = ''
|
||||
form.isLocked = user.isLocked ?? false
|
||||
isHydrating.value = false
|
||||
}
|
||||
|
||||
@@ -129,6 +145,7 @@ async function validate() {
|
||||
const basePayload: UserPayload = {
|
||||
username: normalizedUsername,
|
||||
roles: normalizedRole ? [normalizedRole] : undefined,
|
||||
isLocked: form.isLocked,
|
||||
}
|
||||
if (normalizedPassword) {
|
||||
basePayload.password = normalizedPassword
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
</div>
|
||||
|
||||
<div v-if="auth.isAdmin" class="mt-7 border border-slate-200 mb-11">
|
||||
<div class="grid grid-cols-2 text-primary-700 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||
<div class="grid grid-cols-3 text-primary-700 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
|
||||
<div>Utilisateur</div>
|
||||
<div>Role</div>
|
||||
<div>Statut</div>
|
||||
</div>
|
||||
<div v-if="userList.length === 0" class="px-4 py-6 text-slate-400">
|
||||
Aucun utilisateur.
|
||||
@@ -15,7 +16,7 @@
|
||||
<div
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
class="grid grid-cols-2 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200 items-center"
|
||||
class="grid grid-cols-3 text-primary-700 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200 items-center"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="goToUser(user.id)"
|
||||
@@ -23,6 +24,16 @@
|
||||
>
|
||||
<div>{{ user.username }}</div>
|
||||
<div>{{ getRoleLabels(user.roles) }}</div>
|
||||
<div>
|
||||
<span
|
||||
v-if="user.isLocked"
|
||||
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-red-100 text-red-700"
|
||||
>Verrouillé</span>
|
||||
<span
|
||||
v-else
|
||||
class="inline-block px-2 py-0.5 text-xs font-semibold rounded bg-green-100 text-green-700"
|
||||
>Actif</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
>Mettre en attente
|
||||
</UiButton>
|
||||
</div>
|
||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0" ref="receptionFormRef"/>
|
||||
<WorkflowWeight
|
||||
v-if="storeReception?.currentStep === 1"
|
||||
ref="grossWeightRef"
|
||||
mode="gross"
|
||||
entity-name="reception"
|
||||
api-resource="receptions"
|
||||
@@ -37,6 +38,7 @@
|
||||
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
|
||||
<WorkflowWeight
|
||||
v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3"
|
||||
ref="tareWeightRef"
|
||||
mode="tare"
|
||||
entity-name="reception"
|
||||
api-resource="receptions"
|
||||
@@ -61,8 +63,24 @@ import { RECEPTION_TYPE_CODES } from '~/utils/constants'
|
||||
|
||||
const receptionStore = useReceptionStore()
|
||||
const { current: storeReception } = storeToRefs(receptionStore)
|
||||
const receptionFormRef = ref<{ saveDraft: () => Promise<void>, validateFields: () => boolean } | null>(null)
|
||||
const grossWeightRef = ref<{ saveWeightDraft: () => Promise<void> } | null>(null)
|
||||
const tareWeightRef = ref<{ saveWeightDraft: () => Promise<void> } | null>(null)
|
||||
|
||||
const { stepLabels, saveAndHold, handleStepSelect } = useWorkflowSteps(receptionConfig, receptionStore)
|
||||
const { stepLabels, handleStepSelect } = useWorkflowSteps(receptionConfig, receptionStore)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const saveAndHold = async () => {
|
||||
if (receptionFormRef.value) {
|
||||
if (!receptionFormRef.value.validateFields()) return
|
||||
await receptionFormRef.value.saveDraft()
|
||||
} else {
|
||||
if (grossWeightRef.value) await grossWeightRef.value.saveWeightDraft()
|
||||
if (tareWeightRef.value) await tareWeightRef.value.saveWeightDraft()
|
||||
}
|
||||
await router.push('/')
|
||||
}
|
||||
|
||||
// Init route watcher
|
||||
const route = useRoute()
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
||||
<WorkflowWeight
|
||||
v-if="storeShipment?.currentStep === 1"
|
||||
ref="grossWeightRef"
|
||||
mode="gross"
|
||||
entity-name="shipment"
|
||||
api-resource="shipments"
|
||||
@@ -33,6 +34,7 @@
|
||||
<ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
|
||||
<WorkflowWeight
|
||||
v-if="storeShipment?.currentStep === 3"
|
||||
ref="tareWeightRef"
|
||||
mode="tare"
|
||||
entity-name="shipment"
|
||||
api-resource="shipments"
|
||||
@@ -58,7 +60,9 @@ import { ref, watch } from 'vue'
|
||||
|
||||
const shipmentStore = useShipmentStore()
|
||||
const { current: storeShipment } = storeToRefs(shipmentStore)
|
||||
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
|
||||
const shipmentFormRef = ref<{ saveDraft: () => Promise<void>, validateFields: () => boolean } | null>(null)
|
||||
const grossWeightRef = ref<{ saveWeightDraft: () => Promise<void> } | null>(null)
|
||||
const tareWeightRef = ref<{ saveWeightDraft: () => Promise<void> } | null>(null)
|
||||
|
||||
const { stepLabels, handleStepSelect } = useWorkflowSteps(shipmentConfig, shipmentStore)
|
||||
|
||||
@@ -83,7 +87,11 @@ watch(
|
||||
|
||||
const saveAndHold = async () => {
|
||||
if (shipmentFormRef.value) {
|
||||
if (!shipmentFormRef.value.validateFields()) return
|
||||
await shipmentFormRef.value.saveDraft()
|
||||
} else {
|
||||
if (grossWeightRef.value) await grossWeightRef.value.saveWeightDraft()
|
||||
if (tareWeightRef.value) await tareWeightRef.value.saveWeightDraft()
|
||||
}
|
||||
await router.push('/')
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ export async function logout() {
|
||||
const api = useApi()
|
||||
return api.post<void>('logout', {}, {
|
||||
toastErrorKey: 'errors.auth.logout',
|
||||
toastSuccessKey: 'success.auth.logout',
|
||||
redirect: 'manual'
|
||||
toastSuccessKey: 'success.auth.logout'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,16 +2,19 @@ export interface UserData {
|
||||
id: number
|
||||
username: string
|
||||
roles: string[]
|
||||
isLocked: boolean
|
||||
}
|
||||
|
||||
export type UserPayload = {
|
||||
username?: string
|
||||
password?: string
|
||||
roles?: string[]
|
||||
isLocked?: boolean
|
||||
}
|
||||
|
||||
export type UserFormData = {
|
||||
username: string
|
||||
password: string
|
||||
role: string
|
||||
isLocked: boolean
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import type {UserData} from '~/services/dto/user-data'
|
||||
import {getCurrentUser, createUser, login, logout} from '~/services/auth'
|
||||
import {getCurrentUser, createUser, updateUser, login, logout} from '~/services/auth'
|
||||
import type {UserPayload} from "~/services/dto/user-data";
|
||||
import {ROLE} from '~/utils/constants'
|
||||
|
||||
@@ -58,7 +58,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
},
|
||||
async updateUser(id: number, payload: UserPayload) {
|
||||
this.isLoading = true
|
||||
const result = await createUser(payload).finally(() => {
|
||||
const result = await updateUser(id, payload).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
return result
|
||||
|
||||
31
migrations/Version20260325142815.php
Normal file
31
migrations/Version20260325142815.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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 Version20260325142815 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 "user" ADD is_locked BOOLEAN DEFAULT false NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE public."user" DROP is_locked');
|
||||
}
|
||||
}
|
||||
@@ -75,9 +75,5 @@ chmod o+rx "$(dirname "$DEPLOY_DIR")" "$DEPLOY_DIR" 2>/dev/null || true
|
||||
|
||||
echo "Release ${TAG} deployed to ${DEPLOY_DIR}"
|
||||
|
||||
if [ -f "${DEPLOY_DIR}/.env.local" ]; then
|
||||
echo "Running migrations (if any)..."
|
||||
php "${DEPLOY_DIR}/bin/console" doctrine:migrations:migrate --no-interaction --env=prod
|
||||
else
|
||||
echo "Skip migrations: ${DEPLOY_DIR}/.env.local not found" >&2
|
||||
fi
|
||||
echo "Running migrations (if any)..."
|
||||
php "${DEPLOY_DIR}/bin/console" doctrine:migrations:migrate --no-interaction --env=prod
|
||||
|
||||
@@ -31,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
#[ORM\Table(name: 'reception')]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
||||
#[ApiResource(
|
||||
order: ['id' => 'DESC'],
|
||||
operations: [
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
|
||||
@@ -31,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
#[ORM\Table(name: 'shipment')]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ['isValid'])]
|
||||
#[ApiResource(
|
||||
order: ['id' => 'DESC'],
|
||||
operations: [
|
||||
new Get(
|
||||
requirements: ['id' => '\d+'],
|
||||
|
||||
@@ -9,12 +9,14 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\State\ActiveUsersProvider;
|
||||
use App\State\MeProvider;
|
||||
use App\State\UserPasswordProcessor;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'user', schema: 'public')]
|
||||
@@ -45,7 +47,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
),
|
||||
new GetCollection(
|
||||
normalizationContext: ['groups' => ['user-login:read']],
|
||||
security: "is_granted('PUBLIC_ACCESS')"
|
||||
security: "is_granted('PUBLIC_ACCESS')",
|
||||
provider: ActiveUsersProvider::class
|
||||
),
|
||||
new GetCollection(
|
||||
uriTemplate: '/admin/users',
|
||||
@@ -76,6 +79,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[Groups(['user:write'])]
|
||||
private string $password = '';
|
||||
|
||||
#[ORM\Column(type: 'boolean', options: ['default' => false])]
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
#[SerializedName('isLocked')]
|
||||
private bool $isLocked = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -125,6 +133,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsLocked(): bool
|
||||
{
|
||||
return $this->isLocked;
|
||||
}
|
||||
|
||||
public function setIsLocked(bool $isLocked): self
|
||||
{
|
||||
$this->isLocked = $isLocked;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// No-op: we don't store temporary sensitive data on the entity.
|
||||
|
||||
27
src/Security/UserChecker.php
Normal file
27
src/Security/UserChecker.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final class UserChecker implements UserCheckerInterface
|
||||
{
|
||||
public function checkPreAuth(UserInterface $user): void
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->getIsLocked()) {
|
||||
throw new CustomUserMessageAccountStatusException('Ce compte est verrouillé.');
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void {}
|
||||
}
|
||||
20
src/State/ActiveUsersProvider.php
Normal file
20
src/State/ActiveUsersProvider.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final readonly class ActiveUsersProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
return $this->em->getRepository(User::class)->findBy(['isLocked' => false]);
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@
|
||||
</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;">
|
||||
<div style="display:inline-block; line-height:1.3; border: 1px solid black; padding: 8px; border-radius: 8px; width: 60mm">
|
||||
<strong>{{ reception.supplier ? reception.supplier.name : '-' }}</strong><br>
|
||||
<span>{{ reception.address ? reception.address.street : '' }}</span><br>
|
||||
{% if reception.address and reception.address.street2 %}
|
||||
|
||||
Reference in New Issue
Block a user