Compare commits

...

16 Commits

Author SHA1 Message Date
e1f7d28f0d fix : bloque si bovin superieur a 52 2026-02-26 08:18:37 +01:00
0bb8497871 fix : trie du code et reorganisation composant parent enfant 2026-02-25 15:42:59 +01:00
141f174d0a fix : correction de l'enregistrement des pesées sur l'admin 2026-02-25 10:30:41 +01:00
586f122e3b fix : correction du retour sur la liste des réceptions + suppression de la redirection après enregistrement 2026-02-25 09:28:55 +01:00
0a887a9347 fix : print affichage correctif 2026-02-24 17:07:02 +01:00
945ffc823a Merge remote-tracking branch 'origin/develop' into feat/332-refonte-reception-terminee
# Conflicts:
#	CHANGELOG.md
2026-02-24 14:10:57 +01:00
57551f2b4f feat : changelog 2026-02-24 11:17:56 +01:00
6bd400d1a2 fix : correctif finalisation et correctif couleur de affichage reception finie 2026-02-24 10:30:35 +01:00
86299a18f3 feat : finalisation et correctif couleur de affichage reception finie 2026-02-24 08:59:25 +01:00
d47f237bac feat : dernier push avant finalisation de affichage reception finie 2026-02-23 17:44:33 +01:00
gitea-actions
c52f22472d chore: bump version to v0.0.52
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m19s
2026-02-19 07:47:19 +00:00
e7421e985e [#331] Mettre à jour l'entité Shipment et bovin_shipment (!30)
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|      #331           |        Mettre à jour l'entité Shipment et bovin_shipment         |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [ ] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié

Reviewed-on: #30
Reviewed-by: Autin <tristan@yuno.malio.fr>
Co-authored-by: kevin <kevin@yuno.malio.fr>
Co-committed-by: kevin <kevin@yuno.malio.fr>
2026-02-19 07:47:13 +00:00
gitea-actions
0d258ae9c6 chore: bump version to v0.0.51
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m15s
2026-02-17 13:22:34 +00:00
7dd615ea34 Bovins Admin (!29)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [ ] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié

Reviewed-on: #29
Reviewed-by: Autin <tristan@yuno.malio.fr>
Co-authored-by: Matteo <matteo@yuno.malio.fr>
Co-committed-by: Matteo <matteo@yuno.malio.fr>
2026-02-17 13:22:29 +00:00
gitea-actions
6eee0745a7 chore: bump version to v0.0.50
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m22s
2026-02-17 07:20:06 +00:00
845f94db8c fix : correction du role pour la récupération de la liste des supplier
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
2026-02-17 08:19:56 +01:00
36 changed files with 1337 additions and 1074 deletions

83
.idea/workspace.xml generated
View File

@@ -4,10 +4,13 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : corrections diverses"> <list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : correction du retour sur la liste des réceptions + suppression de la redirection après enregistrement">
<change beforePath="$PROJECT_DIR$/.idea/data_source_mapping.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CHANGELOG.md" beforeDir="false" afterPath="$PROJECT_DIR$/CHANGELOG.md" 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/reception/update-bovin.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/update-bovin.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/update-merchandise.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/update-merchandise.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/update/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/update/[[id]].vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -39,7 +42,7 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY"> <option name="RECENT_BRANCH_BY_REPOSITORY">
<map> <map>
<entry key="$PROJECT_DIR$" value="feat/276-lister-expeditions-terminees" /> <entry key="$PROJECT_DIR$" value="develop" />
</map> </map>
</option> </option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -223,36 +226,36 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;, "RunOnceActivity.MCP Project settings loaded": "true",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;, "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;, "RunOnceActivity.typescript.service.memoryLimit.init": "true",
&quot;git-widget-placeholder&quot;: &quot;fix/325-corrections-diverses&quot;, "git-widget-placeholder": "feat/332-refonte-reception-terminee",
&quot;last_opened_file_path&quot;: &quot;//wsl.localhost/Ubuntu-24.04/home/kevin/Stage/Ferme/frontend/pages/shipment&quot;, "last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/kevin/Stage/Ferme/frontend/pages/shipment",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "nodejs_package_manager_path": "npm",
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;, "settings.editor.selected.configurable": "preferences.pluginManager",
&quot;ts.external.directory.path&quot;: &quot;/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external&quot;, "ts.external.directory.path": "/opt/phpstorm/plugins/javascript-plugin/jsLanguageServicesImpl/external",
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; "vue.rearranger.settings.migration": "true"
}, },
&quot;keyToStringList&quot;: { "keyToStringList": {
&quot;DatabaseDriversLRU&quot;: [ "DatabaseDriversLRU": [
&quot;postgresql&quot; "postgresql"
], ],
&quot;com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File&quot;: [ "com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
&quot;TEXT&quot; "TEXT"
], ],
&quot;vue.recent.templates&quot;: [ "vue.recent.templates": [
&quot;Vue Composition API Component&quot; "Vue Composition API Component"
] ]
} }
}</component> }]]></component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <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\pages\shipment" />
@@ -309,14 +312,6 @@
<workItem from="1770966186589" duration="914000" /> <workItem from="1770966186589" duration="914000" />
<workItem from="1770967274060" duration="2388000" /> <workItem from="1770967274060" duration="2388000" />
</task> </task>
<task id="LOCAL-00007" summary="test : ajout de TU sur les services et providers">
<option name="closed" value="true" />
<created>1768318921478</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1768318921478</updated>
</task>
<task id="LOCAL-00008" summary="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global"> <task id="LOCAL-00008" summary="feat : ajout de la génération du bon de reception, correction de la base du formulaire multi-etape de reception et ajout d'une gestion d'erreur global">
<option name="closed" value="true" /> <option name="closed" value="true" />
<created>1768498751836</created> <created>1768498751836</created>
@@ -701,7 +696,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1770969471135</updated> <updated>1770969471135</updated>
</task> </task>
<option name="localTasksCounter" value="56" /> <task id="LOCAL-00056" summary="fix : correction du retour sur la liste des réceptions + suppression de la redirection après enregistrement">
<option name="closed" value="true" />
<created>1772008192160</created>
<option name="number" value="00056" />
<option name="presentableId" value="LOCAL-00056" />
<option name="project" value="LOCAL" />
<updated>1772008192161</updated>
</task>
<option name="localTasksCounter" value="57" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -751,7 +754,6 @@
</option> </option>
</component> </component>
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value="fix : gitea workflow" />
<MESSAGE value="fix : script de déploiement" /> <MESSAGE value="fix : script de déploiement" />
<MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" /> <MESSAGE value="feat : ajout plus d'information sur la liste des réceptions côté front sur la page d'accueil" />
<MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" /> <MESSAGE value="fix : redirige sur le login sur une 401 et reset du auth state + doc + timeout du toaster" />
@@ -776,7 +778,8 @@
<MESSAGE value="feat : lister les expeditions terminees" /> <MESSAGE value="feat : lister les expeditions terminees" />
<MESSAGE value="fix: corrections diverses" /> <MESSAGE value="fix: corrections diverses" />
<MESSAGE value="fix : corrections diverses" /> <MESSAGE value="fix : corrections diverses" />
<option name="LAST_COMMIT_MESSAGE" value="fix : corrections diverses" /> <MESSAGE value="fix : correction du retour sur la liste des réceptions + suppression de la redirection après enregistrement" />
<option name="LAST_COMMIT_MESSAGE" value="fix : correction du retour sur la liste des réceptions + suppression de la redirection après enregistrement" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>

View File

@@ -47,7 +47,10 @@ Ajouter dans le fichier .env du frontend
* [#326] Admin modification creation client * [#326] Admin modification creation client
* [#325] Correction diverses * [#325] Correction diverses
* fix layout admin * fix layout admin
* Creation page admin listing bovins
* Creation page admin ajout/modification bovins
* [#331] Mettre à jour l'entité Shipment et bovin_shipment
* [#332] Refonte écran réception terminée
### Changed ### Changed
### Fixed ### Fixed

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.0.49' app.version: '0.0.52'

View File

@@ -2,17 +2,17 @@
<template> <template>
<NuxtLink :to="link"> <NuxtLink :to="link">
<div class="w-[300px] h-[216px] border border-black rounded-lg p-6 flex flex-col justify-between gap-4"> <div class="w-[300px] h-[216px] border border-primary-700 rounded-lg p-6 flex flex-col justify-between gap-4">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="rounded-full w-[80px] h-[80px] bg-[#D9D9D9] flex justify-center items-center"> <div class="rounded-full w-[80px] h-[80px] bg-[#D9D9D9] flex justify-center items-center">
<Icon :name="iconName" style="color: black" size="44" /> <Icon :name="iconName" class="!text-primary-700" size="44" />
</div> </div>
<div> <div>
<Icon name="mdi:plus" style="color: black" size="44" /> <Icon name="mdi:plus" style="color: black" size="44" />
</div> </div>
</div> </div>
<div class="uppercase font-bold"> <div class="uppercase font-bold">
<p class="text-3xl text-primary-500"> <p class="text-3xl text-primary-700">
<slot name="label">{{ label }}</slot> <slot name="label">{{ label }}</slot>
</p> </p>
</div> </div>

View File

@@ -1,14 +1,14 @@
<template> <template>
<div <div
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS" v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
class="flex flex-col items-center gap-16"> class="flex flex-col gap-16">
<h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1> <h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
<div <div
class="flex flex-row gap-8 items-center"> class="flex flex-row gap-8 items-center w-full">
<div <div
v-for="type in bovineType" v-for="type in bovineType"
:key="type.id" :key="type.id"
class="mt-8 flex flex-row mb-2 gap-6"> class="mt-8 flex flex-row mb-2 w-full">
<UiNumberInput <UiNumberInput
:id="type.id" :id="type.id"
:label="type.label" :label="type.label"
@@ -17,6 +17,8 @@
:placeholder="0" :placeholder="0"
:min="0" :min="0"
:max="10" :max="10"
class="max-w-[150px]"
wrapper-class="gap-3"
/> />
</div> </div>
<div <div
@@ -24,6 +26,8 @@
<UiNumberInput <UiNumberInput
label="Autres" label="Autres"
v-model="otherQuantity" v-model="otherQuantity"
class="max-w-[80px]"
wrapper-class="gap-3"
/> />
</div> </div>
</div> </div>

View File

@@ -1,183 +1,113 @@
<template> <template>
<form @submit.prevent="validate"> <form>
<div class="flex flex-row justify-between gap-x-12 font-bold uppercase mb-8">
<div <div
class="flex flex-col items-center gap-16"> v-for="type in bovineTypes"
<div
class="flex flex-row gap-6 items-center">
<div
v-for="type in bovineType"
:key="type.id" :key="type.id"
class="flex flex-row mb-2 gap-6 "> >
<UiNumberInput <UiNumberInput
:label="type.label" :label="type.label"
:code="type.code" :code="type.code"
v-model="bovineQuantities[String(type.id)]" v-model="localQuantities[String(type.id)]"
:disabled="!auth.isAdmin" :disabled="!isAdmin"
:placeholder="0" :placeholder="0"
:min="0" :min="0"
:max="10" :max="10"
wrapperClass="w-44 flex-col"
/> />
</div> </div>
<div
class=" flex flex-row mb-2 gap-6">
<UiNumberInput <UiNumberInput
label="Autres" label="Autres"
v-model="otherQuantity" v-model="localOtherQuantity"
:disabled="!auth.isAdmin" :disabled="!isAdmin"
wrapperClass="w-44 flex-col"
/> />
</div> </div>
</div>
<UiButton
type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
:disabled="!auth.isAdmin"
>Valider
</UiButton>
</div>
</form> </form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {BovineTypeData} from "~/services/dto/bovine-type-data"; import { onMounted, reactive, ref, watch } from 'vue'
import {getBovineTypeList} from "~/services/bovine-type"; import { getBovineTypeList } from '~/services/bovine-type'
import { import type { BovineTypeData } from '~/services/dto/bovine-type-data'
createReceptionBovine, import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
deleteReceptionBovine,
getReceptionBovineList,
updateReceptionBovine
} from "~/services/reception-bovine";
import {computed, onMounted, reactive, ref, watch} from "vue";
import {getReception, updateReception} from "~/services/reception";
const toast = useToast()
const isLoadingBovineType = ref(false)
const bovineType = ref<BovineTypeData[]>([])
const bovineQuantities = reactive<Record<string, number | null>>({})
const otherQuantity = ref<number | null>(0)
const auth = useAuthStore()
const props = defineProps<{ const props = defineProps<{
idReception: number modelValue: ReceptionBovineTypeData[]
otherQuantity: number | null
isAdmin: boolean
}>() }>()
const receptionId = props.idReception
const reception = await getReception(receptionId)
const receptionIri = computed(() => const emit = defineEmits<{
receptionId ? `/api/receptions/${receptionId}` : null (event: 'update:modelValue', value: ReceptionBovineTypeData[]): void
) (event: 'update:otherQuantity', value: number | null): void
const totalBovines = computed(() => { }>()
const base = Object.values(bovineQuantities).reduce((sum, value) => {
return sum + (value ?? 0) const bovineTypes = ref<BovineTypeData[]>([])
}, 0) const localQuantities = reactive<Record<string, number | null>>({})
return base + (otherQuantity.value ?? 0) const localOtherQuantity = ref<number | null>(props.otherQuantity ?? 0)
const isSyncing = ref(false)
function buildEntriesFromLocal(): ReceptionBovineTypeData[] {
return bovineTypes.value.map((type) => {
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
return {
id: existing?.id ?? 0,
bovineType: type,
quantity: localQuantities[String(type.id)] ?? 0
}
}) })
}
const loadBovineType = async () => { function syncLocalFromProps() {
isLoadingBovineType.value = true isSyncing.value = true
try { try {
bovineType.value = await getBovineTypeList() for (const key of Object.keys(localQuantities)) {
delete localQuantities[key]
}
for (const type of bovineTypes.value) {
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
localQuantities[String(type.id)] = existing?.quantity ?? 0
}
} finally { } finally {
isLoadingBovineType.value = false isSyncing.value = false
} }
} }
onMounted(async () => { watch(
await loadBovineType() () => props.otherQuantity,
(value) => {
localOtherQuantity.value = value ?? 0
}
)
watch(localOtherQuantity, (value) => {
emit('update:otherQuantity', value ?? 0)
}) })
watch( watch(
[() => receptionId, () => bovineType.value], () => props.modelValue,
async ([id, types]) => { () => {
if (!id || !receptionIri.value || types.length === 0) { syncLocalFromProps()
return
}
const selectionMap: Record<string, number | null> = {}
for (const type of types) {
selectionMap[String(type.id)] = 0
}
const existing = await getReceptionBovineList(receptionIri.value)
for (const selection of existing) {
const bovineTypeId = String(selection.bovineType.id)
selectionMap[bovineTypeId] = selection.quantity ?? 0
}
for (const key of Object.keys(bovineQuantities)) {
delete bovineQuantities[key]
}
Object.assign(bovineQuantities, selectionMap)
const existingOther = reception.bovineDetail
const parsedOther =
typeof existingOther === 'string' && existingOther.trim() !== ''
? Number(existingOther)
: 0
otherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
}, },
{immediate: true} { deep: true }
) )
async function syncBovineSelections(receptionIri: string) { watch(
const existing = await getReceptionBovineList(receptionIri) localQuantities,
const existingMap = new Map<string, { id: number; quantity: number | null }>() () => {
if (isSyncing.value) {
for (const selection of existing) {
const bovineTypeId = String(selection.bovineType.id)
existingMap.set(bovineTypeId, {
id: selection.id,
quantity: selection.quantity ?? 0
})
}
// Supprime les entrées supprimées ou modifiées
for (const [bovineTypeId, entry] of existingMap.entries()) {
const selectedQuantity = bovineQuantities[bovineTypeId] ?? 0
if (!selectedQuantity) {
await deleteReceptionBovine(entry.id)
existingMap.delete(bovineTypeId)
continue
}
if (selectedQuantity !== entry.quantity) {
await updateReceptionBovine(entry.id, {quantity: selectedQuantity})
existingMap.set(bovineTypeId, {
id: entry.id,
quantity: selectedQuantity
})
}
}
// Crée les entrées manquantes
for (const [bovineTypeId, quantity] of Object.entries(bovineQuantities)) {
if (!quantity) {
continue
}
if (existingMap.has(bovineTypeId)) {
// Déjà à jour
continue
}
await createReceptionBovine({
reception: receptionIri,
bovineType: `/api/bovine_types/${bovineTypeId}`,
quantity
})
}
}
async function validate() {
// @TODO Ajouter un composable pour le toaster qui gère les key i18n
if (totalBovines.value > 52) {
toast.error({
title: 'Erreur',
message: ('Le total des bovins ne peut pas dépasser 52.')
})
return return
} }
emit('update:modelValue', buildEntriesFromLocal())
},
{ deep: true }
)
await syncBovineSelections(receptionIri.value) onMounted(async () => {
bovineTypes.value = await getBovineTypeList()
await updateReception(receptionId, { syncLocalFromProps()
merchandiseType: null, emit('update:modelValue', buildEntriesFromLocal())
merchandiseDetail: null,
bovineDetail: otherQuantity.value ? String(otherQuantity.value) : null,
}) })
}
</script> </script>

View File

@@ -1,33 +1,27 @@
<template> <template>
<form @submit.prevent="validate"> <form>
<div class="flex flex-col items-center gap-16"> <div class="flex flex-col">
<div <div class="w-full col-start-1 row-start-1">
class="flex flex-col gap-16 items-center w-full"> <UiRadioGroup
<UiTextInput
id="merchandise-type" id="merchandise-type"
v-model="selectedMerchandiseTypeId" v-model="selectedMerchandiseTypeId"
label="Type de marchandises" label="Type de marchandises"
:value="reception.merchandiseType?.label" :options="merchandiseTypes.map((type) => ({
wrapper-class="w-[550px]" value: String(type.id),
:disabled="true" label: type.label
/> }))"
<div input-class="accent-primary-700 focus:ring-primary-700"
v-if="merchandiseTypeId && isAutres" option-label-class="uppercase"
class="flex flex-col w-full max-w-[550px]" wrapper-class="w-full uppercase"
> group-class="grid grid-cols-[336px_336px_355px_200px] w-[160px_160px_200px_180px] mt-9 mb-7"
<UiTextInput :disabled="!isAdmin"
id="merchandise-detail"
:disabled="!auth.isAdmin"
v-model="merchandiseDetail"
label="Préciser"
placeholder="Précisions complémentaires"
:maxlength="255"
/> />
</div> </div>
<div class="w-full grid grid-cols-[3fr_1fr] gap-12 col-start-2 row-start-1">
<div <div
v-if="merchandiseTypeId && !isGranule" v-if="selectedMerchandiseTypeId && !isGranule"
class="flex gap-4 w-[550px] justify-evenly" class="flex gap-[218px]"
> >
<div <div
v-for="building in buildings" v-for="building in buildings"
@@ -37,29 +31,46 @@
v-model="selectedBuildingIds" v-model="selectedBuildingIds"
:value="String(building.id)" :value="String(building.id)"
:label="building.label" :label="building.label"
:disabled="!auth.isAdmin" :disabled="!isAdmin"
label-class="text-xl" input-class="accent-primary-700 focus:ring-primary-700"
label-class="text-xl uppercase"
/> />
</div> </div>
</div> </div>
<div <div
v-if="merchandiseTypeId && isGranule" v-if="selectedMerchandiseTypeId && isAutres"
class="flex flex-col gap-10 w-full max-w-[1100px]" class="flex flex-col justify-self-end max-w-[182px]"
> >
<div class="grid grid-cols-1 gap-10 md:grid-cols-4"> <UiTextInput
id="merchandise-detail"
:disabled="!isAdmin"
v-model="merchandiseDetail"
placeholder="Préciser"
:maxlength="255"
class="h-6"
/>
</div>
</div>
<div
v-if="selectedMerchandiseTypeId && isGranule"
class="flex flex-col gap-10 w-full col-start-2 row-start-1"
>
<div class="grid grid-cols-1 md:grid-cols-[max-content_max-content_max-content_max-content] justify-between">
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4"> <div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
<p class="font-bold uppercase">{{ type.label }}</p> <p class="mb-1 font-medium uppercase">{{ type.label }}</p>
<div <div
v-for="building in buildings" v-for="building in buildings"
:key="building.id" :key="building.id"
class="flex items-center gap-2 text-lg" class="flex text-lg"
> >
<UiCheckbox <UiCheckbox
v-model="selectedPelletBuildingIds[String(type.id)]" v-model="selectedPelletBuildingIds[String(type.id)]"
:value="String(building.id)" :value="String(building.id)"
:label="building.label" :label="building.label"
:disabled="!auth.isAdmin" :disabled="!isAdmin"
input-class="accent-primary-700 focus:ring-primary-700"
label-class="text-lg" label-class="text-lg"
/> />
</div> </div>
@@ -67,82 +78,127 @@
</div> </div>
</div> </div>
</div> </div>
<UiButton
v-if="auth.isAdmin"
type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
:disabled="!auth.isAdmin"
>Valider
</UiButton>
</div>
</form> </form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, ref} from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import type { BuildingData } from '~/services/dto/building-data'
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
import type { MerchandiseEntryData } from '~/services/dto/reception-data'
import { getBuildingList } from '~/services/building' import { getBuildingList } from '~/services/building'
import { getMerchandiseTypeList } from '~/services/merchandise-type' import { getMerchandiseTypeList } from '~/services/merchandise-type'
import type {MerchandiseTypeData} from '~/services/dto/merchandise-type-data'
import type {BuildingData} from '~/services/dto/building-data'
import type {PelletTypeData} from '~/services/dto/pellet-type-data'
import { getPelletTypeList } from '~/services/pellet-type' import { getPelletTypeList } from '~/services/pellet-type'
import {
createReceptionPelletBuilding,
deleteReceptionPelletBuilding,
getReceptionPelletBuildingList
} from '~/services/reception-pellet-building'
import { MERCHANDISE_TYPE_CODES } from '~/utils/constants' import { MERCHANDISE_TYPE_CODES } from '~/utils/constants'
import {getReception, updateReception} from "~/services/reception";
const props = defineProps<{
modelValue: MerchandiseEntryData
isAdmin: boolean
}>()
const emit = defineEmits<{
(event: 'update:modelValue', value: MerchandiseEntryData): void
}>()
const merchandiseTypes = ref<MerchandiseTypeData[]>([]) const merchandiseTypes = ref<MerchandiseTypeData[]>([])
const buildings = ref<BuildingData[]>([]) const buildings = ref<BuildingData[]>([])
const pelletTypes = ref<PelletTypeData[]>([]) const pelletTypes = ref<PelletTypeData[]>([])
const selectedMerchandiseTypeId = ref('') const selectedMerchandiseTypeId = ref('')
const selectedBuildingIds = ref<string[]>([]) const selectedBuildingIds = ref<string[]>([])
const selectedPelletBuildingIds = ref<Record<string, string[]>>({}) const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
const merchandiseDetail = ref('') const merchandiseDetail = ref('')
const auth = useAuthStore() const isHydrating = ref(false)
const props = defineProps<{ const isReady = ref(false)
idReception: number
}>()
const receptionId = props.idReception
const reception = await getReception(receptionId)
const merchandiseTypeId = await reception.receptionType?.id
// Extrait l'ID d'une relation depuis un IRI ou un objet complet.
const getRelationId = (value: unknown): string | null => {
if (!value) {
return null
}
if (typeof value === 'string') {
const match = value.match(/\/(\d+)$/)
return match ? match[1] : null
}
if (typeof value === 'object' && 'id' in value) {
const record = value as { id?: number | string }
if (typeof record.id === 'number') {
return String(record.id)
}
if (typeof record.id === 'string') {
return record.id
}
}
return null
}
// Type de marchandise sélectionné dans le select
const selectedMerchandiseType = computed(() => const selectedMerchandiseType = computed(() =>
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value) merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value) ?? null
)
const isGranule = computed(
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE
)
const isAutres = computed(
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES
)
function clonePelletSelections(value: Record<string, string[]>) {
const clone: Record<string, string[]> = {}
for (const [key, buildingIds] of Object.entries(value)) {
clone[key] = [...buildingIds]
}
return clone
}
function ensurePelletKeys() {
for (const pelletType of pelletTypes.value) {
const key = String(pelletType.id)
if (!selectedPelletBuildingIds.value[key]) {
selectedPelletBuildingIds.value[key] = []
}
}
}
function hydrateFromModelValue(value: MerchandiseEntryData) {
isHydrating.value = true
try {
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
merchandiseDetail.value = value.merchandiseDetail ?? ''
selectedBuildingIds.value = [...(value.selectedBuildingIds ?? [])]
selectedPelletBuildingIds.value = clonePelletSelections(
value.selectedPelletBuildingIds ?? {}
)
ensurePelletKeys()
} finally {
isHydrating.value = false
}
}
function emitModelValue() {
emit('update:modelValue', {
merchandiseTypeId: selectedMerchandiseTypeId.value,
merchandiseDetail: merchandiseDetail.value,
selectedBuildingIds: [...selectedBuildingIds.value],
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
})
}
watch(
() => props.modelValue,
(value) => {
hydrateFromModelValue(value)
},
{ deep: true }
)
watch(
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
() => {
if (isHydrating.value || !isReady.value) {
return
}
if (isGranule.value) {
if (selectedBuildingIds.value.length > 0) {
selectedBuildingIds.value = []
}
} else {
for (const key of Object.keys(selectedPelletBuildingIds.value)) {
if (selectedPelletBuildingIds.value[key].length > 0) {
selectedPelletBuildingIds.value[key] = []
}
}
}
if (!isAutres.value && merchandiseDetail.value !== '') {
merchandiseDetail.value = ''
}
emitModelValue()
},
{ deep: true }
) )
// Indique si le type est "Granulé"
const isGranule = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE)
// Indique si le type est "Autres"
const isAutres = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES)
// Charge les référentiels et hydrate le formulaire depuis la réception
onMounted(async () => { onMounted(async () => {
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([ const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
getMerchandiseTypeList(), getMerchandiseTypeList(),
@@ -153,106 +209,8 @@ onMounted(async () => {
buildings.value = buildingList buildings.value = buildingList
pelletTypes.value = pelletTypeList pelletTypes.value = pelletTypeList
const currentId = reception.merchandiseType?.id hydrateFromModelValue(props.modelValue)
if (currentId) { isReady.value = true
selectedMerchandiseTypeId.value = String(currentId) emitModelValue()
}
merchandiseDetail.value = reception.merchandiseDetail ?? ''
selectedBuildingIds.value =
reception.buildings?.map((building) => String(building.id)) ?? []
const existingPelletSelections = reception.pelletBuildings ?? []
const selectionMap: Record<string, string[]> = {}
for (const selection of existingPelletSelections) {
// L'API peut renvoyer les relations comme IRI ou comme objets selon le contexte.
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
if (!selectionMap[pelletTypeId]) {
selectionMap[pelletTypeId] = []
}
selectionMap[pelletTypeId].push(buildingId)
}
for (const pelletType of pelletTypes.value) {
const key = String(pelletType.id)
if (!selectionMap[key]) {
selectionMap[key] = []
}
}
selectedPelletBuildingIds.value = selectionMap
}) })
// Enregistre les sélections et passe à l'étape suivante
async function validate() {
const receptionIri = `/api/receptions/${reception.id}`
await updateReception(reception.id, {
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() : null,
buildings: isGranule.value
? []
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
bovineDetail: null,
bovinesTypes: null,
})
if (isGranule.value) {
await syncPelletSelections(receptionIri)
} else {
await clearPelletSelections(receptionIri)
}
}
// Supprime toutes les associations granulés/bâtiments existantes
async function clearPelletSelections(receptionIri: string) {
const existing = await getReceptionPelletBuildingList(receptionIri)
for (const selection of existing) {
await deleteReceptionPelletBuilding(selection.id)
}
}
// Synchronise les associations granulés/bâtiments avec l'état du formulaire
async function syncPelletSelections(receptionIri: string) {
const existing = await getReceptionPelletBuildingList(receptionIri)
const existingMap = new Map<string, number>()
for (const selection of existing) {
// Construit la table de correspondance avec des IDs normalisés pour éviter les doublons.
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
const key = `${pelletTypeId}:${buildingId}`
existingMap.set(key, selection.id)
}
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
for (const [pelletTypeId, buildingIds] of Object.entries(selectedPelletBuildingIds.value)) {
for (const buildingId of buildingIds) {
desiredEntries.push({pelletTypeId, buildingId})
}
}
const desiredKeys = new Set(desiredEntries.map(
(entry) => `${entry.pelletTypeId}:${entry.buildingId}`
))
for (const [key, id] of existingMap.entries()) {
if (!desiredKeys.has(key)) {
await deleteReceptionPelletBuilding(id)
}
}
for (const entry of desiredEntries) {
const key = `${entry.pelletTypeId}:${entry.buildingId}`
if (!existingMap.has(key)) {
await createReceptionPelletBuilding({
reception: receptionIri,
pelletType: `/api/pellet_types/${entry.pelletTypeId}`,
building: `/api/buildings/${entry.buildingId}`
})
}
}
}
</script> </script>

View File

@@ -1,124 +1,63 @@
<template> <template>
<form @submit.prevent="validate"> <form>
<div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-8"> <div class="grid grid-cols-3 gap-x-40 gap-y-8 mb-8">
<UiNumberInput
:key="localWeight.type"
:label="'POIDS'"
labelClass="font-bold uppercase text-xl "
v-model="localWeight.weight"
:disabled="!isAdmin"
:min="0"
:max="48000"
wrapper-class="flex-col"
/>
<UiDateInput
label="Date pesée"
v-model="localWeight.weighedAt"
:disabled="!isAdmin"
/>
<UiNumberInput <UiNumberInput
label="Dsd" label="Dsd"
class="col-start-2" class="col-start-2"
labelClass="font-bold uppercase" labelClass="font-bold uppercase"
v-model="sharedWeightMeta.dsd" v-model="localWeight.dsd"
:disabled="!auth.isAdmin" :disabled="!isAdmin"
/> wrapper-class="flex-col"
<UiDateInput
label="Date pesée"
v-model="sharedWeightMeta.weighedAt"
:disabled="!auth.isAdmin"
/> />
</div> </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"
inputClass="w-24"
v-model="weight.weight"
:wrapper-class="weight.type === 'tare' ? 'col-start-1 row-start-1' : 'col-start-2 row-start-1'"
:disabled="!auth.isAdmin"
:min="0"
:max="48000"
/>
</div>
<div class="flex justify-center">
<UiButton
v-if="auth.isAdmin"
type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
>
Valider
</UiButton>
</div>
</form> </form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {ReceptionFormWeight} from '~/services/dto/reception-data' import type {WeightEntryData} from '~/services/dto/reception-data'
import {getReception} from '~/services/reception' import {reactive, watch} from "vue";
import {updateWeight} from "~/services/weight";
import {useAuthStore} from "~/stores/auth";
const props = defineProps<{ const props = defineProps<{
idReception: number modelValue: WeightEntryData
isAdmin: boolean
}>() }>()
const idReception = props.idReception const emit = defineEmits<{
const auth = useAuthStore() (event: 'update:modelValue', value: WeightEntryData): void
}>()
const form = reactive({ const localWeight = reactive<WeightEntryData>({...props.modelValue})
weights: [
{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 => { watch(
return type === 'tare' ? 'Pesée à vide' : 'Pesée à plein' () => props.modelValue,
} (value) => {
Object.assign(localWeight, value)
const hydrateFromReception = (reception: ReceptionFormWeight) => { },
// On hydrate chaque ligne par son type (tare/gross), sans dépendre d'un index. {deep: true}
for (const receptionWeight of reception.weights) {
const formWeight = form.weights.find(weight => weight.type === receptionWeight.type)
if (formWeight) {
Object.assign(formWeight, receptionWeight)
}
}
// 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) { watch(
sharedWeightMeta.dsd = weightWithMeta.dsd ?? null localWeight,
sharedWeightMeta.weighedAt = weightWithMeta.weighedAt ?? null (value) => {
} emit('update:modelValue', {...value})
} },
{deep: true}
onMounted(async () => { )
const reception = await getReception(idReception)
hydrateFromReception(reception)
})
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,
dsd: Number.isFinite(sharedDsd) ? sharedDsd : null,
weighedAt: sharedWeighedAt
})
}
}
}
</script> </script>

View File

@@ -155,12 +155,6 @@ import {useShipmentStore} from '~/stores/shipment'
import {computed, reactive, ref, watch, onMounted} from 'vue' import {computed, reactive, ref, watch, onMounted} from 'vue'
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data"; import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
import {getShipmentTypeList} from "~/services/shipment-type"; import {getShipmentTypeList} from "~/services/shipment-type";
import {
createShipmentBovine,
deleteShipmentBovine,
getBovinShipmentList,
updateShipmentBovine
} from "~/services/bovin-shipment";
const users = ref<UserData[]>([]) const users = ref<UserData[]>([])
const customers = ref<CustomerData[]>([]) const customers = ref<CustomerData[]>([])
@@ -332,23 +326,15 @@ watch(
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : '' form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : '' form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : '' form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
if (!shipment || !shipment.bovinShipments) {
selectedShipmentTypeId.value = ''
shipmentQuantity.value = 0
} else {
const selectedEntry = shipment.bovinShipments.find((entry) => {
const typeId = entry.shipmentType?.id
return Boolean(typeId) && Number(entry.nbBovinSend ?? 0) > 0
}) ?? shipment.bovinShipments.find((entry) => Boolean(entry.shipmentType?.id))
if (!selectedEntry?.shipmentType?.id) {
selectedShipmentTypeId.value = '' selectedShipmentTypeId.value = shipment?.shipmentType?.id
shipmentQuantity.value = 0 ? String(shipment.shipmentType.id)
} else { : ''
selectedShipmentTypeId.value = String(selectedEntry.shipmentType.id)
shipmentQuantity.value = selectedEntry.nbBovinSend ?? 0 shipmentQuantity.value = shipment?.nbBovinSend ?? 0
}
}
isHydrating.value = false isHydrating.value = false
}, },
{immediate: true} {immediate: true}
@@ -474,68 +460,7 @@ watch(
} }
} }
) )
const buildDesiredBovinShipments = () => {
const typeId = Number(selectedShipmentTypeId.value)
if (!Number.isFinite(typeId)) {
return []
}
const type = bovineShipment.value.find((entry) => entry.id === typeId)
if (!type) {
return []
}
const raw = shipmentQuantity.value
const quantity = raw === null || raw === undefined ? 0 : Number(raw)
const normalizedQuantity = Number.isFinite(quantity) ? Math.max(0, Math.trunc(quantity)) : 0
if (normalizedQuantity <= 0) {
return []
}
return [{type, quantity: normalizedQuantity}]
}
const syncBovinShipments = async (
shipmentId: number,
existing: Array<{ id?: number; nbBovinSend: number | null; shipmentType?: unknown }> = []
) => {
const shipmentIri = `/api/shipments/${shipmentId}`
const desired = buildDesiredBovinShipments()
const desiredByTypeId = new Map<number, number>()
for (const entry of desired) {
desiredByTypeId.set(entry.type.id, entry.quantity)
}
for (const entry of existing) {
if (!entry.id) {
continue
}
const rawType = entry.shipmentType
let typeId: number | null = null
if (rawType && typeof rawType === 'object' && 'id' in rawType) {
typeId = Number((rawType as { id: number }).id)
} else if (typeof rawType === 'string') {
const match = rawType.match(/\/shipment_types\/(\\d+)$/)
typeId = match ? Number(match[1]) : null
}
if (!typeId) {
continue
}
const desiredQuantity = desiredByTypeId.get(typeId)
if (!desiredQuantity) {
await deleteShipmentBovine(entry.id)
continue
}
if (entry.nbBovinSend !== desiredQuantity) {
await updateShipmentBovine(entry.id, {nbBovinSend: desiredQuantity})
}
desiredByTypeId.delete(typeId)
}
for (const [typeId, quantity] of desiredByTypeId.entries()) {
await createShipmentBovine({
shipment: shipmentIri,
shipmentType: `/api/shipment_types/${typeId}`,
nbBovinSend: quantity
})
}
}
const buildPayload = () => { const buildPayload = () => {
const normalizedLicensePlate = form.licencePlate.trim() const normalizedLicensePlate = form.licencePlate.trim()
const normalizedShipmentDate = form.shipmentDate.trim() const normalizedShipmentDate = form.shipmentDate.trim()
@@ -563,6 +488,14 @@ const buildPayload = () => {
const addressIri = normalizedAddressId const addressIri = normalizedAddressId
? `/api/addresses/${normalizedAddressId}` ? `/api/addresses/${normalizedAddressId}`
: null : null
const normalizedShipmentTypeId = selectedShipmentTypeId.value.trim()
const shipmentTypeIri = normalizedShipmentTypeId
? `/api/shipment_types/${normalizedShipmentTypeId}`
: null
const rawQuantity = Number(shipmentQuantity.value ?? 0)
const normalizedQuantity = Number.isFinite(rawQuantity) ? Math.max(0,
Math.trunc(rawQuantity)) : 0
return { return {
licencePlate: normalizedLicensePlate, licencePlate: normalizedLicensePlate,
@@ -572,20 +505,19 @@ const buildPayload = () => {
carrier: carrierIri, carrier: carrierIri,
driver: driverIri, driver: driverIri,
user: userIri, user: userIri,
address: addressIri address: addressIri,
shipmentType: shipmentTypeIri,
nbBovinSend: normalizedQuantity,
} }
} }
const saveDraft = async () => { const saveDraft = async () => {
const payload = buildPayload() const payload = buildPayload()
if (!shipmentStore.current) { if (!shipmentStore.current) {
const created = await shipmentStore.createShipment({ await shipmentStore.createShipment({
currentStep: 0, currentStep: 0,
...payload ...payload
}) })
if (created) {
await syncBovinShipments(created.id, [])
}
return return
} }
@@ -593,10 +525,6 @@ const saveDraft = async () => {
currentStep: shipmentStore.current.currentStep, currentStep: shipmentStore.current.currentStep,
...payload ...payload
}) })
await syncBovinShipments(
shipmentStore.current.id,
shipmentStore.current?.bovinShipments ?? []
)
} }
defineExpose({saveDraft}) defineExpose({saveDraft})
@@ -610,7 +538,6 @@ const validate = async () => {
}) })
if (created) { if (created) {
await shipmentStore.loadShipment(created.id) await shipmentStore.loadShipment(created.id)
await syncBovinShipments(created.id, shipmentStore.current?.bovinShipments ?? [])
await router.push(`/shipment/${created.id}`) await router.push(`/shipment/${created.id}`)
} }
return return
@@ -621,6 +548,5 @@ const validate = async () => {
...payload ...payload
}) })
await shipmentStore.loadShipment(shipmentStore.current.id) await shipmentStore.loadShipment(shipmentStore.current.id)
await syncBovinShipments(shipmentStore.current.id, shipmentStore.current?.bovinShipments ?? [])
} }
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col items-center gap-[118px]"> <div class="flex flex-col items-center gap-[118px]">
<h1 class="font-bold text-5xl uppercase text-primary-500">Charment des bovins</h1> <h1 class="font-bold text-5xl uppercase text-primary-500">Chargement des bovins</h1>
<div <div
class="w-full flex flex-col items-center justify-center"> class="w-full flex flex-col items-center justify-center">
<UiLoadingDots /> <UiLoadingDots />

View File

@@ -1,14 +1,14 @@
<template> <template>
<div :class="wrapperClass"> <div :class="wrapperClass">
<label <label
class="flex items-center gap-2 cursor-pointer text-primary-500" class="flex items-center gap-2 cursor-pointer text-primary-700"
:class="labelClass" :class="labelClass"
> >
<input <input
type="checkbox" type="checkbox"
:checked="checked" :checked="checked"
:disabled="disabled" :disabled="disabled"
:class="['cursor-pointer text-primary-500', inputClass]" :class="['h-4 w-4 cursor-pointer text-primary-500', inputClass]"
@change="onChange" @change="onChange"
> >
<span v-if="label">{{ label }}</span> <span v-if="label">{{ label }}</span>

View File

@@ -3,7 +3,7 @@
<label <label
v-if="label" v-if="label"
:for="id" :for="id"
class="font-bold uppercase text-xl text-primary-500" class="font-bold uppercase text-xl text-primary-700"
:class="labelClass" :class="labelClass"
> >
{{ label }} {{ label }}
@@ -14,9 +14,9 @@
:value="modelValue ?? ''" :value="modelValue ?? ''"
:disabled="disabled" :disabled="disabled"
v-bind="attrs" v-bind="attrs"
class="border-b border-black justify-self-start text-xl text-primary-500 py-[6px] uppercase bg-transparent appearance-none h-[34px]" class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
:class="[ :class="[
isEmpty ? 'text-neutral-400' : 'text-black', isEmpty ? 'text-neutral-400' : 'text-primary-700',
disabled ? 'cursor-not-allowed' : 'cursor-pointer', disabled ? 'cursor-not-allowed' : 'cursor-pointer',
inputClass inputClass
]" ]"

View File

@@ -1,9 +1,10 @@
// flex row passer en class wraper class flex col ainsi que le wfull 34
<template> <template>
<div :class="['flex flex-row items-center gap-2', wrapperClass]"> <div :class="['flex', wrapperClass]">
<label <label
v-if="label" v-if="label"
:for="id" :for="id"
class="text-xl flex items-center gap-2 text-primary-500" class="text-xl flex items-center gap-2 text-primary-700"
:class="labelClass" :class="labelClass"
> >
<span <span
@@ -25,7 +26,7 @@
:step="step" :step="step"
:disabled="disabled" :disabled="disabled"
v-bind="attrs" v-bind="attrs"
class="border-b border-black text-xl bg-transparent w-16 text-primary-500" class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
:class="[ :class="[
isEmpty ? 'text-neutral-400' : 'text-black', isEmpty ? 'text-neutral-400' : 'text-black',
disabled ? 'cursor-not-allowed' : 'cursor-text', disabled ? 'cursor-not-allowed' : 'cursor-text',

View File

@@ -2,7 +2,7 @@
<div :class="['flex flex-col', wrapperClass]"> <div :class="['flex flex-col', wrapperClass]">
<label <label
v-if="label" v-if="label"
class="font-bold uppercase text-xl text-primary-500" class="font-bold uppercase text-xl text-primary-700"
:class="labelClass" :class="labelClass"
> >
{{ label }} {{ label }}
@@ -16,7 +16,7 @@
v-for="option in options" v-for="option in options"
:key="String(option.value)" :key="String(option.value)"
:for="`${id || 'radio'}-${option.value}`" :for="`${id || 'radio'}-${option.value}`"
class="flex items-center gap-2 text-primary-500" class="flex items-center gap-2 text-primary-700"
:class="itemClass" :class="itemClass"
> >
<input <input
@@ -27,7 +27,7 @@
:checked="String(modelValue ?? '') === String(option.value)" :checked="String(modelValue ?? '') === String(option.value)"
:disabled="disabled" :disabled="disabled"
v-bind="attrs" v-bind="attrs"
class="h-4 w-4 border-slate-300 text-primary-500 focus:ring-primary-500" class="h-4 w-4 border-primary-700/50 text-primary-700 focus:ring-primary-700"
:class="[ :class="[
disabled ? 'cursor-not-allowed' : 'cursor-pointer', disabled ? 'cursor-not-allowed' : 'cursor-pointer',
inputClass inputClass

View File

@@ -3,7 +3,7 @@
<label <label
v-if="label" v-if="label"
:for="id" :for="id"
class="font-bold uppercase text-xl text-primary-500" class="font-bold uppercase text-xl text-primary-700"
:class="labelClass" :class="labelClass"
> >
{{ label }} {{ label }}
@@ -13,9 +13,9 @@
:value="modelValue ?? ''" :value="modelValue ?? ''"
:disabled="disabled || loading" :disabled="disabled || loading"
v-bind="attrs" v-bind="attrs"
class="border-b border-black justify-self-start text-xl text-primary-500 py-[6px] bg-transparent" class="border-b border-primary-700 justify-self-start text-xl text-primary-700 py-[6px] bg-transparent"
:class="[ :class="[
isEmpty ? 'text-neutral-400' : 'text-black', isEmpty ? 'text-neutral-400' : 'text-primary-700',
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer', disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
selectClass selectClass
]" ]"
@@ -28,7 +28,7 @@
v-for="option in options" v-for="option in options"
:key="option.value" :key="option.value"
:value="option.value" :value="option.value"
class="text-black" class="text-primary-700"
> >
{{ option.label }} {{ option.label }}
</option> </option>

View File

@@ -105,6 +105,23 @@
Clients Clients
</a> </a>
</NuxtLink> </NuxtLink>
<NuxtLink
v-if="auth.isAdmin"
to="/admin/bovin/list"
custom
v-slot="{ href, navigate }"
>
<a
:href="href"
@click="navigate"
:class="route.path.startsWith('/admin/bovin')
? 'opacity-100'
: 'opacity-65 hover:opacity-100 transition'"
>
Bovins
</a>
</NuxtLink>
</nav> </nav>
<!-- Spacer mobile (pour centrer visuellement le header si besoin) --> <!-- Spacer mobile (pour centrer visuellement le header si besoin) -->
@@ -118,7 +135,8 @@
aria-haspopup="true" aria-haspopup="true"
> >
<span class="capitalize font-bold">{{ userDisplayName }}</span> <span class="capitalize font-bold">{{ userDisplayName }}</span>
<span class="ml-[6px] inline-flex items-center font-bold transition-transform group-hover:rotate-180 group-focus-within:rotate-180"> <span
class="ml-[6px] inline-flex items-center font-bold transition-transform group-hover:rotate-180 group-focus-within:rotate-180">
<Icon name="mdi:chevron-down" size="20"/> <Icon name="mdi:chevron-down" size="20"/>
</span> </span>
</button> </button>
@@ -196,6 +214,9 @@
<NuxtLink v-if="auth.isAdmin" to="/admin/customer/customer-list" @click="closeMenu"> <NuxtLink v-if="auth.isAdmin" to="/admin/customer/customer-list" @click="closeMenu">
Clients Clients
</NuxtLink> </NuxtLink>
<NuxtLink v-if="auth.isAdmin" to="/admin/bovin/list" @click="closeMenu">
Bovins
</NuxtLink>
</nav> </nav>
<button <button

View File

@@ -0,0 +1,104 @@
<template>
<form @submit.prevent="validate">
<div class="text-primary-500 flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase">
{{ route.params.id ? 'Modifier bovin' : 'Ajout bovin' }}
</h1>
<UiButton
type="submit"
:disabled="isLoading || isHydrating"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] hover:opacity-80"
>
<Icon name="mdi:check" size="28" />
{{ isEdit ? 'Modifier' : 'Ajouter' }}
</UiButton>
</div>
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 py-12">
<UiTextInput label="Nom du bovin" id="bovin-label" v-model="form.label" />
<UiTextInput label="Code bovin" id="code-id" v-model="form.code" />
</div>
</form>
</template>
<script setup lang="ts">
import {createBovin, getBovin, updateBovin} from "~/services/bovine-type";
import type {BovineTypeData, BovinFormData} from "~/services/dto/bovine-type-data";
const router = useRouter()
const route = useRoute()
const isLoading = ref(false)
const isHydrating = ref(false)
function 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 idBovin = computed(() => resolveId(route.params.id))
const isEdit = computed(() => idBovin.value !== null)
const form = reactive<BovinFormData>({
label: '',
code: ''
})
const hydrateFromBovin = (bovin: BovineTypeData | null) => {
if (!bovin) {
return
}
isHydrating.value = true
form.label = bovin.label ?? ''
form.code = bovin.code ?? ''
isHydrating.value = false
}
watch(
() => idBovin.value,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const bovin = await getBovin(id)
hydrateFromBovin(bovin)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
async function validate() {
if (isLoading.value || isHydrating.value) return
const normalizedBovinCode = form.code.trim()
const normalizedBovinLabel = form.label.trim()
const basePayload = {
label: normalizedBovinLabel,
code: normalizedBovinCode
}
isLoading.value = true
try {
if (isEdit.value && idBovin.value !== null) {
await updateBovin(idBovin.value, basePayload)
} else {
await createBovin(basePayload)
}
await navigate()
} finally {
isLoading.value = false
}
}
async function navigate(){
return router.push("/admin/bovin/list")
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold text-primary-500 uppercase">Liste des types bovins</h1>
<NuxtLink
to="/admin/bovin"
class="inline-flex items-center justify-center
text-xl text-white uppercase
bg-primary-500 h-[50px] px-8 rounded
hover:opacity-80 gap-2"
@click="handleAddClick"
>
<Icon name="mdi:plus" size="28" />
Ajouter
</NuxtLink>
</div>
<div v-if="auth.isAdmin" class="mt-6 border border-slate-200 mb-16">
<div class="max-h-96 overflow-y-auto">
<div
class="sticky
grid grid-cols-2 gap-4
bg-slate-100 px-4 py-3
font-semibold uppercase
tracking-wide"
>
<div class="col-span-1">Nom</div>
<div class="col-span-1">Code</div>
</div>
<div v-if="bovinList.length === 0" class="px-4 py-6 text-slate-400">
Aucun type de bovin.
</div>
<div v-else>
<div
v-for="bovin in bovinList"
:key="bovin.id"
class="grid grid-cols-2 border-t gap-4 px-4 py-2 hover:bg-slate-50 cursor-pointer"
@click="goToBovin(bovin.id)"
>
<div class="col-span-1">{{ bovin.label }}</div>
<div class="col-span-1">{{ bovin.code }}</div>
</div>
</div>
</div>
</div>
<div v-else class="mt-6 border border-slate-200 mb-16 px-4 py-6 text-slate-400">
Accès réservé aux administrateurs.
</div>
</template>
<script setup lang="ts">
import { getBovineTypeList } from "~/services/bovine-type"
import type { BovineTypeData } from "~/services/dto/bovine-type-data"
import { useAuthStore } from "~/stores/auth"
const bovinList = ref<BovineTypeData[]>([])
const router = useRouter()
const auth = useAuthStore()
const goToBovin = (id: number) => {
if (!auth.isAdmin) return
router.push(`/admin/bovin/${id}`)
}
const handleAddClick = (event: Event) => {
if (auth.isAdmin) return
event.preventDefault()
}
onMounted(async () => {
if (!auth.isAdmin) return
bovinList.value = await getBovineTypeList()
})
</script>

View File

@@ -1,10 +1,14 @@
<template> <template>
<form @submit.prevent="validate"> <form @submit.prevent="validate">
<div class="flex items-center justify-between mt-12 mb-8 "> <div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-[60px]">
<h1 class="font-bold text-5xl uppercase">Réception {{ receptionLoad?.identificationNumber }}</h1> <div class="flex items-center justify-between gap-10 relative">
<div class="flex flex-row absolute -left-[60px] justify-between">
<Icon @click="router.push('/reception/finish-reception')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
</div>
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Réception {{ form.identificationNumber }}</h1>
<Icon @click="printReceipt" name="mdi:printer-outline" size="44" class="cursor-pointer text-primary-500"/>
</div> </div>
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-12">
<!-- Nom de l'utilisateur --> <!-- Nom de l'utilisateur -->
<UiSelect <UiSelect
id="reception-user" id="reception-user"
@@ -16,7 +20,7 @@
label: user.username label: user.username
}))" }))"
:loading="isLoadingUsers" :loading="isLoadingUsers"
wrapper-class="col-start-1 row-start-1" wrapper-class="col-start-1 row-start-2"
/> />
<!-- Date de réception --> <!-- Date de réception -->
<UiDateInput <UiDateInput
@@ -24,7 +28,20 @@
:disabled="!auth.isAdmin" :disabled="!auth.isAdmin"
v-model="form.receptionDate" v-model="form.receptionDate"
label="Date de réception" label="Date de réception"
wrapper-class="col-start-1 row-start-2" wrapper-class="col-start-1 row-start-3"
/>
<!-- type de reception -->
<UiSelect
id="reception-supplier"
v-model="form.receptionTypeId"
:disabled="!auth.isAdmin"
label="Type de Réception"
:options="receptionTypes.map((receptionType) => ({
value: String(receptionType.id),
label: receptionType.label
}))"
:loading="isLoadingSuppliers"
wrapper-class="col-start-1 row-start-4"
/> />
<!-- Fournisseur --> <!-- Fournisseur -->
<UiSelect <UiSelect
@@ -37,7 +54,7 @@
label: supplier.name label: supplier.name
}))" }))"
:loading="isLoadingSuppliers" :loading="isLoadingSuppliers"
wrapper-class="col-start-1 row-start-3" wrapper-class="col-start-1 row-start-5"
/> />
<!-- Adresse fournisseur --> <!-- Adresse fournisseur -->
<UiSelect <UiSelect
@@ -49,7 +66,7 @@
label: address.fullAddress label: address.fullAddress
}))" }))"
:disabled="(isLoadingSuppliers || supplierAddresses.length === 0) && !auth.isAdmin" :disabled="(isLoadingSuppliers || supplierAddresses.length === 0) && !auth.isAdmin"
wrapper-class="col-start-1 row-start-4" wrapper-class="col-start-2 row-start-1"
/> />
<!-- Camion --> <!-- Camion -->
<UiSelect <UiSelect
@@ -62,7 +79,7 @@
label: truck.name label: truck.name
}))" }))"
:loading="isLoadingTrucks" :loading="isLoadingTrucks"
wrapper-class="col-start-2 row-start-1" wrapper-class="col-start-2 row-start-2"
/> />
<!-- Transporteur --> <!-- Transporteur -->
<UiSelect <UiSelect
@@ -76,7 +93,7 @@
}))" }))"
:loading="isLoadingCarriers" :loading="isLoadingCarriers"
select-class="h-[34px]" select-class="h-[34px]"
wrapper-class="col-start-2 row-start-2" wrapper-class="col-start-2 row-start-3"
/> />
<!-- Chauffeur (LIOT) --> <!-- Chauffeur (LIOT) -->
<UiSelect <UiSelect
@@ -89,7 +106,8 @@
label: driver.name label: driver.name
}))" }))"
:loading="isLoadingDrivers" :loading="isLoadingDrivers"
wrapper-class="col-start-2 row-start-3" wrapper-class="col-start-2 row-start-5"
v-if="isLiotCarrier"
/> />
<!-- Plaque d'immatriculation --> <!-- Plaque d'immatriculation -->
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4"> <div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
@@ -114,7 +132,59 @@
wrapper-class="col-start-2 row-start-4" wrapper-class="col-start-2 row-start-4"
/> />
</div> </div>
<div class="flex justify-center mb-2"> <div v-if="formIsLoading">
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
<h1
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer "
:class="activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
@click="activeTab = 'weights'"
>
pesée à plein
</h1>
<h1
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer "
:class="activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500 ' : 'text-primary-500/50'"
@click="activeTab = 'weightsEmpty'"
>
pesée à vide
</h1>
<h1
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer "
:class="activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
@click="activeTab = 'merchandise'"
>
{{ isMerchandise ? "Marchandise" : "Bovins" }}
</h1>
</div>
<div class="mb-12 ">
<update-weight
v-show="activeTab === 'weights'"
v-model="grossWeight"
v-if="grossWeight"
:isAdmin="auth.isAdmin"
/>
<update-weight
v-show="activeTab === 'weightsEmpty'"
v-model="tareWeight"
v-if="tareWeight"
:isAdmin="auth.isAdmin"
/>
<update-merchandise
v-show="activeTab === 'merchandise' && isMerchandise"
v-model="merchandiseForm"
:isAdmin="auth.isAdmin"
/>
<update-bovin
v-show="activeTab === 'merchandise' && !isMerchandise"
v-model="bovineEntries"
v-model:otherQuantity="bovineOtherQuantity"
:isAdmin="auth.isAdmin"
/>
</div>
<div class="flex justify-center">
<UiButton <UiButton
v-if="auth.isAdmin" v-if="auth.isAdmin"
type="submit" type="submit"
@@ -124,70 +194,96 @@
Enregistrer Enregistrer
</UiButton> </UiButton>
</div> </div>
<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> </div>
<update-weight
v-if="activeTab === 'weights'"
: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> </form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useReceptionStore} from '~/stores/reception' import { usePdfPrinter } from '#imports'
import type {UserData} from '~/services/dto/user-data' import { computed } from 'vue'
import UpdateBovin from '~/components/reception/update-bovin.vue'
import UpdateMerchandise from '~/components/reception/update-merchandise.vue'
import UpdateWeight from '~/components/reception/update-weight.vue'
import { getUsers } from '~/services/auth' import { getUsers } from '~/services/auth'
import {useAuthStore} from '~/stores/auth'
import type {SupplierData} from '~/services/dto/supplier-data'
import {getSupplierList} from '~/services/supplier'
import type {TruckData} from '~/services/dto/truck-data'
import {getTruckList} from '~/services/truck'
import type {CarrierData} from '~/services/dto/carrier-data'
import { getCarrierList } from '~/services/carrier' import { getCarrierList } from '~/services/carrier'
import type { CarrierData } from '~/services/dto/carrier-data'
import type { DriverData } from '~/services/dto/driver-data' import type { DriverData } from '~/services/dto/driver-data'
import {getDriverList} from '~/services/driver' import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
import type {
MerchandiseEntryData,
ReceptionData,
ReceptionFormData,
WeightEntryData
} from '~/services/dto/reception-data'
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
import type { SupplierData } from '~/services/dto/supplier-data'
import type { TruckData } from '~/services/dto/truck-data'
import type { UserData } from '~/services/dto/user-data'
import type { VehicleData } from '~/services/dto/vehicle-data' import type { VehicleData } from '~/services/dto/vehicle-data'
import { getDriverList } from '~/services/driver'
import {
createReceptionBovine,
deleteReceptionBovine,
getReceptionBovineList,
updateReceptionBovine
} from '~/services/reception-bovine'
import {
createReceptionPelletBuilding,
deleteReceptionPelletBuilding,
getReceptionPelletBuildingList
} from '~/services/reception-pellet-building'
import { getReception, updateReception } from '~/services/reception'
import { getReceptionTypeList } from '~/services/reception-type'
import { getSupplierList } from '~/services/supplier'
import { getTruckList } from '~/services/truck'
import { getVehicleList } from '~/services/vehicle' import { getVehicleList } from '~/services/vehicle'
import {SUPPLIER_CODE} from "~/utils/constants"; import { createWeight, updateWeight } from '~/services/weight'
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine"; import { useAuthStore } from '~/stores/auth'
import type {ReceptionData, ReceptionFormData} from "~/services/dto/reception-data"; import { useReceptionStore } from '~/stores/reception'
import {getReception} from "~/services/reception"; import { RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
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 router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const authStore = useAuthStore()
const receptionStore = useReceptionStore() const receptionStore = useReceptionStore()
const { printPdf } = usePdfPrinter()
const activeTab = ref<'weightsEmpty' | 'weights' | 'merchandise'>('weights')
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
const bovineEntries = ref<ReceptionBovineTypeData[]>([])
const bovineOtherQuantity = ref<number | null>(0)
const merchandiseForm = ref<MerchandiseEntryData>({
merchandiseTypeId: '',
merchandiseDetail: '',
selectedBuildingIds: [],
selectedPelletBuildingIds: {}
})
const allowAnyLicensePlate = ref(false)
const isLoading = ref(false)
const users = ref<UserData[]>([])
const isLoadingUsers = ref(false)
const isLoadingTypes = ref(false)
const suppliers = ref<SupplierData[]>([])
const receptionTypes = ref<ReceptionTypeData[]>([])
const isLoadingSuppliers = ref(false)
const trucks = ref<TruckData[]>([])
const isLoadingTrucks = ref(false)
const carriers = ref<CarrierData[]>([])
const isLoadingCarriers = ref(false)
const drivers = ref<DriverData[]>([])
const isLoadingDrivers = ref(false)
const vehicles = ref<VehicleData[]>([])
const isLoadingVehicles = ref(false)
const formIsLoading = ref(false)
const isMerchandise = ref(false)
const isHydrating = ref(false)
const idReception = Number(route.params.id)
const form = reactive<ReceptionFormData>({ const form = reactive<ReceptionFormData>({
identificationNumber: null,
licensePlate: '', licensePlate: '',
receptionDate: new Date().toISOString().slice(0, 10), receptionDate: new Date().toISOString().slice(0, 10),
receptionTypeId: '', receptionTypeId: '',
@@ -199,39 +295,11 @@ const form = reactive<ReceptionFormData>({
driverId: '', driverId: '',
vehicleId: '' vehicleId: ''
}) })
const allowAnyLicensePlate = ref(false)
const isLoading = ref(false)
const users = ref<UserData[]>([])
const isLoadingUsers = ref(false)
const suppliers = ref<SupplierData[]>([])
const isLoadingSuppliers = ref(false)
const trucks = ref<TruckData[]>([])
const isLoadingTrucks = ref(false)
const carriers = ref<CarrierData[]>([])
const isLoadingCarriers = ref(false)
const drivers = ref<DriverData[]>([])
const isLoadingDrivers = ref(false)
const vehicles = ref<VehicleData[]>([])
const isLoadingVehicles = ref(false)
const authStore = useAuthStore()
// Empêche les watchers de reset des champs pendant le remplissage initial
const isHydrating = ref(false)
const route = useRoute()
const idReception = Number(route.params.id)
const receptionLoad = await getReception(idReception)
const receptionType = receptionLoad.receptionType
const auth = useAuthStore()
const isBtWeight = ref(true)
const isMerchandise = ref(receptionType.code === 'MARCHANDISES')
// Transporteur sélectionné dans le formulaire
const selectedCarrier = computed(() => const selectedCarrier = computed(() =>
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
) )
// Indique si le transporteur est LIOT
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT) const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
// Adresses disponibles pour le fournisseur sélectionné
const supplierAddresses = computed(() => { const supplierAddresses = computed(() => {
const supplierId = Number(form.supplierId) const supplierId = Number(form.supplierId)
if (!Number.isFinite(supplierId)) { if (!Number.isFinite(supplierId)) {
@@ -239,14 +307,12 @@ const supplierAddresses = computed(() => {
} }
return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? [] return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? []
}) })
// Chauffeurs filtrés par transporteur (LIOT)
const filteredDrivers = computed<DriverData[]>(() => { const filteredDrivers = computed<DriverData[]>(() => {
if (!form.carrierId) { if (!form.carrierId) {
return [] return []
} }
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId) return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
}) })
// Véhicules filtrés par transporteur + type de camion
const filteredVehicles = computed<VehicleData[]>(() => { const filteredVehicles = computed<VehicleData[]>(() => {
if (!form.carrierId) { if (!form.carrierId) {
return [] return []
@@ -258,19 +324,102 @@ const filteredVehicles = computed<VehicleData[]>(() => {
) )
}) })
// Supprime les données bovines si on change de type de réception watch(
const clearReceptionBovines = async (receptionIri: string) => { () => idReception,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const reception = await getReception(id)
hydrateFromReception(reception)
await loadBovineEntries(id)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
watch(
() => form.receptionTypeId,
() => {
const receptionType = receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
}
)
function createEmptyWeightEntry(type: 'gross' | 'tare'): WeightEntryData {
return {
type,
dsd: null,
weight: null,
weighedAt: null
}
}
async function clearReceptionBovines(receptionId: number) {
const receptionIri = `/api/receptions/${receptionId}`
const existing = await getReceptionBovineList(receptionIri) const existing = await getReceptionBovineList(receptionIri)
for (const selection of existing) { for (const selection of existing) {
await deleteReceptionBovine(selection.id) await deleteReceptionBovine(selection.id)
} }
} }
const hydrateFromUser = (reception: ReceptionData | null) => { async function loadBovineEntries(receptionId: number) {
const receptionIri = `/api/receptions/${receptionId}`
bovineEntries.value = await getReceptionBovineList(receptionIri)
}
function syncMerchandiseFlag() {
const receptionType =
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
}
function getRelationId(value: unknown): string | null {
if (!value) {
return null
}
if (typeof value === 'string') {
const match = value.match(/\/(\d+)$/)
return match ? match[1] : null
}
if (typeof value === 'object' && 'id' in value) {
const relation = value as { id?: number | string }
if (typeof relation.id === 'number') {
return String(relation.id)
}
if (typeof relation.id === 'string') {
return relation.id
}
}
return null
}
async function clearReceptionMerchandise(receptionId: number) {
const receptionIri = `/api/receptions/${receptionId}`
const existing = await getReceptionPelletBuildingList(receptionIri)
for (const selection of existing) {
await deleteReceptionPelletBuilding(selection.id)
}
await updateReception(receptionId, {
merchandiseType: null,
merchandiseDetail: null,
buildings: []
})
}
function hydrateFromReception(reception: ReceptionData | null) {
if (!reception) { if (!reception) {
return return
} }
isHydrating.value = true isHydrating.value = true
form.identificationNumber = reception?.identificationNumber ?? ''
form.licensePlate = reception?.licensePlate ?? '' form.licensePlate = reception?.licensePlate ?? ''
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10) form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
form.userId = reception?.user?.id form.userId = reception?.user?.id
@@ -291,28 +440,44 @@ const hydrateFromUser = (reception: ReceptionData | null) => {
form.driverId = reception?.driver?.id form.driverId = reception?.driver?.id
? String(reception.driver.id) ? String(reception.driver.id)
: '' : ''
form.receptionTypeId = reception?.receptionType?.id
? String(reception.receptionType.id)
: ''
const selectionMap: Record<string, string[]> = {}
for (const selection of reception?.pelletBuildings ?? []) {
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
if (!selectionMap[pelletTypeId]) {
selectionMap[pelletTypeId] = []
}
selectionMap[pelletTypeId].push(buildingId)
}
merchandiseForm.value = {
merchandiseTypeId: reception?.merchandiseType?.id ? String(reception.merchandiseType.id) : '',
merchandiseDetail: reception?.merchandiseDetail ?? '',
selectedBuildingIds: reception?.buildings?.map((building) => String(building.id)) ?? [],
selectedPelletBuildingIds: selectionMap
}
const parsedOther =
typeof reception?.bovineDetail === 'string' && reception.bovineDetail.trim() !== ''
? Number(reception.bovineDetail)
: 0
bovineOtherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
const gross = reception.weights?.find((weight) => weight.type === 'gross') ?? null
const tare = reception.weights?.find((weight) => weight.type === 'tare') ?? null
grossWeight.value = gross ? { ...gross } : createEmptyWeightEntry('gross')
tareWeight.value = tare ? { ...tare } : createEmptyWeightEntry('tare')
isHydrating.value = false isHydrating.value = false
syncMerchandiseFlag()
} }
watch( async function loadUsers() {
() => idReception,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const user = await getReception(id)
hydrateFromUser(user)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
// Charge la liste des users pour le select
const loadUsers = async () => {
isLoadingUsers.value = true isLoadingUsers.value = true
try { try {
users.value = await getUsers() users.value = await getUsers()
@@ -321,8 +486,7 @@ const loadUsers = async () => {
} }
} }
// Charge la liste des fournisseurs pour le select async function loadSuppliers() {
const loadSuppliers = async () => {
isLoadingSuppliers.value = true isLoadingSuppliers.value = true
try { try {
suppliers.value = await getSupplierList() suppliers.value = await getSupplierList()
@@ -331,8 +495,16 @@ const loadSuppliers = async () => {
} }
} }
// Charge la liste des camions pour le select async function loadTypes() {
const loadTrucks = async () => { isLoadingTypes.value = true
try {
receptionTypes.value = await getReceptionTypeList()
} finally {
isLoadingSuppliers.value = false
}
}
async function loadTrucks() {
isLoadingTrucks.value = true isLoadingTrucks.value = true
try { try {
trucks.value = await getTruckList() trucks.value = await getTruckList()
@@ -341,8 +513,7 @@ const loadTrucks = async () => {
} }
} }
// Charge la liste des transporteurs pour le select async function loadCarriers() {
const loadCarriers = async () => {
isLoadingCarriers.value = true isLoadingCarriers.value = true
try { try {
carriers.value = await getCarrierList() carriers.value = await getCarrierList()
@@ -351,8 +522,7 @@ const loadCarriers = async () => {
} }
} }
// Charge la liste des chauffeurs pour le select async function loadDrivers() {
const loadDrivers = async () => {
isLoadingDrivers.value = true isLoadingDrivers.value = true
try { try {
drivers.value = await getDriverList() drivers.value = await getDriverList()
@@ -361,8 +531,7 @@ const loadDrivers = async () => {
} }
} }
// Charge la liste des véhicules pour le select async function loadVehicles() {
const loadVehicles = async () => {
isLoadingVehicles.value = true isLoadingVehicles.value = true
try { try {
vehicles.value = await getVehicleList() vehicles.value = await getVehicleList()
@@ -371,8 +540,7 @@ const loadVehicles = async () => {
} }
} }
// On met le user connecté par défaut dans le select function setDefaultUser() {
const setDefaultUser = () => {
if (form.userId) { if (form.userId) {
return return
} }
@@ -381,8 +549,253 @@ const setDefaultUser = () => {
} }
} }
// On récupère toutes les données des selects au chargement du composant async function printReceipt() {
if (!import.meta.client || !Number.isFinite(idReception) || idReception <= 0) {
return
}
const supplierName =
suppliers.value.find((supplier) => String(supplier.id) === form.supplierId)?.name ??
'fournisseur'
const filename = `${form.identificationNumber || idReception}_${supplierName}_${form.licensePlate || 'immat'}.pdf`
await printPdf(`/receptions/${idReception}/receipt`, filename)
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
await new Promise((resolve) => setTimeout(resolve, 600))
}
async function saveWeightEntry(entry: WeightEntryData) {
if (!idReception || entry.weight === null) {
return
}
const payload = {
type: entry.type,
dsd: entry.dsd ?? null,
weight: entry.weight,
weighedAt: entry.weighedAt ?? null
}
if (entry.id) {
await updateWeight(entry.id, payload)
return
}
await createWeight({
reception: `api/receptions/${idReception}`,
...payload
})
}
async function saveBovineEntry(entry: ReceptionBovineTypeData) {
if (!idReception || !entry.bovineType || entry.quantity === null || entry.quantity <= 0) {
return
}
const payload = {
quantity: entry.quantity
}
if (entry.id) {
await updateReceptionBovine(entry.id, payload)
return
}
await createReceptionBovine({
reception: `/api/receptions/${idReception}`,
bovineType: `/api/bovine_types/${entry.bovineType.id}`,
...payload
})
}
function getTotalBovines() {
const totalTypes = bovineEntries.value.reduce(
(sum, entry) => sum + (entry.quantity ?? 0),
0
)
return totalTypes +
(bovineOtherQuantity.value ?? 0)
}
async function syncBovineEntries() {
if (!idReception) {
return
}
const receptionIri = `/api/receptions/${idReception}`
const existing = await getReceptionBovineList(receptionIri)
const currentPositive = bovineEntries.value.filter(
(entry) => (entry.quantity ?? 0) > 0
)
for (const existingEntry of existing) {
const stillPresent = currentPositive.some(
(entry) => entry.bovineType.id === existingEntry.bovineType.id
)
if (!stillPresent) {
await deleteReceptionBovine(existingEntry.id)
}
}
for (const entry of currentPositive) {
await saveBovineEntry(entry)
}
}
async function saveMerchandiseEntry(
receptionIri: string,
pelletTypeId: string,
buildingId: string,
existingKeys: Set<string>
) {
const key = `${pelletTypeId}:${buildingId}`
if (existingKeys.has(key)) {
return
}
await createReceptionPelletBuilding({
reception: receptionIri,
pelletType: `/api/pellet_types/${pelletTypeId}`,
building: `/api/buildings/${buildingId}`
})
}
async function syncMerchandiseEntries() {
if (!idReception) {
return
}
const receptionIri = `/api/receptions/${idReception}`
const existing = await getReceptionPelletBuildingList(receptionIri)
const existingMap = new Map<string, number>()
for (const selection of existing) {
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
existingMap.set(`${pelletTypeId}:${buildingId}`, selection.id)
}
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
for (const [pelletTypeId, buildingIds] of Object.entries(
merchandiseForm.value.selectedPelletBuildingIds
)) {
for (const buildingId of buildingIds) {
desiredEntries.push({ pelletTypeId, buildingId })
}
}
const desiredKeys = new Set(
desiredEntries.map((entry) => `${entry.pelletTypeId}:${entry.buildingId}`)
)
for (const [key, selectionId] of existingMap.entries()) {
if (!desiredKeys.has(key)) {
await deleteReceptionPelletBuilding(selectionId)
}
}
const existingKeys = new Set(existingMap.keys())
for (const entry of desiredEntries) {
await saveMerchandiseEntry(
receptionIri,
entry.pelletTypeId,
entry.buildingId,
existingKeys
)
}
}
async function validate() {
const normalizedLicensePlate = form.licensePlate.trim()
const normalizedReceptionDate = form.receptionDate.trim()
const normalizedReceptionTypeId = form.receptionTypeId.trim()
const normalizedUserId = form.userId.trim()
const normalizedSupplierId = form.supplierId.trim()
const normalizedAddressId = form.addressId.trim()
const normalizedTruckId = form.truckId.trim()
const normalizedCarrierId = form.carrierId.trim()
const normalizedDriverId = form.driverId.trim()
const userIri = normalizedUserId
? `/api/users/${normalizedUserId}`
: null
const supplierIri = normalizedSupplierId
? `/api/suppliers/${normalizedSupplierId}`
: null
const addressIri = normalizedAddressId
? `/api/addresses/${normalizedAddressId}`
: null
const truckIri = normalizedTruckId
? `/api/trucks/${normalizedTruckId}`
: null
const carrierIri = normalizedCarrierId
? `/api/carriers/${normalizedCarrierId}`
: null
const driverIri = normalizedDriverId
? `/api/drivers/${normalizedDriverId}`
: null
const typeIri = normalizedReceptionTypeId
? `/api/reception_types/${normalizedReceptionTypeId}`
: null
const basePayload = {
licensePlate: normalizedLicensePlate,
receptionDate: normalizedReceptionDate,
receptionType: typeIri,
user: userIri,
supplier: supplierIri,
address: addressIri,
truck: truckIri,
carrier: carrierIri
}
const payload = {
...basePayload,
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {}),
}
if (idReception) {
await receptionStore.updateReception(idReception, {
...payload
})
await saveWeightEntry(grossWeight.value)
await saveWeightEntry(tareWeight.value)
if (isMerchandise.value) {
await clearReceptionBovines(idReception)
await updateReception(idReception, {
merchandiseType: merchandiseForm.value.merchandiseTypeId
? `/api/merchandise_types/${merchandiseForm.value.merchandiseTypeId}`
: null,
merchandiseDetail: merchandiseForm.value.merchandiseDetail.trim() || null,
buildings: merchandiseForm.value.selectedBuildingIds.map(
(buildingId) => `/api/buildings/${buildingId}`
),
bovineDetail: null,
bovinesTypes: null
})
await syncMerchandiseEntries()
} else {
if (getTotalBovines() > 52) {
// toast/erreur UI
return
}
await clearReceptionMerchandise(idReception)
await syncBovineEntries()
await updateReception(idReception, {
bovineDetail: bovineOtherQuantity.value ? String(bovineOtherQuantity.value) : null
})
}
const refreshedReception = await getReception(idReception)
hydrateFromReception(refreshedReception)
return
}
}
onMounted(async () => { onMounted(async () => {
await loadTypes()
syncMerchandiseFlag()
formIsLoading.value = true
await loadUsers() await loadUsers()
await loadSuppliers() await loadSuppliers()
await loadTrucks() await loadTrucks()
@@ -393,7 +806,6 @@ onMounted(async () => {
setDefaultUser() setDefaultUser()
}) })
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
watch( watch(
() => [form.supplierId, form.addressId, suppliers.value], () => [form.supplierId, form.addressId, suppliers.value],
() => { () => {
@@ -422,7 +834,6 @@ watch(
{ immediate: true } { immediate: true }
) )
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
watch( watch(
() => form.carrierId, () => form.carrierId,
() => { () => {
@@ -449,7 +860,6 @@ watch(
{ immediate: true } { immediate: true }
) )
// Récupère la plaque depuis le véhicule choisi (LIOT)
watch( watch(
() => [form.truckId, form.carrierId, vehicles.value], () => [form.truckId, form.carrierId, vehicles.value],
() => { () => {
@@ -473,7 +883,6 @@ watch(
{ immediate: true } { immediate: true }
) )
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
watch( watch(
() => [form.vehicleId, form.carrierId, vehicles.value], () => [form.vehicleId, form.carrierId, vehicles.value],
() => { () => {
@@ -508,60 +917,4 @@ watch(
} }
) )
// Valide le formulaire et crée/met à jour la réception
async function validate() {
const normalizedLicensePlate = form.licensePlate.trim()
const normalizedReceptionDate = form.receptionDate.trim()
const normalizedUserId = form.userId.trim()
const normalizedSupplierId = form.supplierId.trim()
const normalizedAddressId = form.addressId.trim()
const normalizedTruckId = form.truckId.trim()
const normalizedCarrierId = form.carrierId.trim()
const normalizedDriverId = form.driverId.trim()
const userIri = normalizedUserId
? `/api/users/${normalizedUserId}`
: null
const supplierIri = normalizedSupplierId
? `/api/suppliers/${normalizedSupplierId}`
: null
const addressIri = normalizedAddressId
? `/api/addresses/${normalizedAddressId}`
: null
const truckIri = normalizedTruckId
? `/api/trucks/${normalizedTruckId}`
: null
const carrierIri = normalizedCarrierId
? `/api/carriers/${normalizedCarrierId}`
: null
const driverIri = normalizedDriverId
? `/api/drivers/${normalizedDriverId}`
: null
const basePayload = {
licensePlate: normalizedLicensePlate,
receptionDate: normalizedReceptionDate,
user: userIri,
supplier: supplierIri,
address: addressIri,
truck: truckIri,
carrier: carrierIri
}
const payload = {
...basePayload,
...(isLiotCarrier.value && driverIri ? {driver: driverIri} : {})
}
if (idReception) {
const updated = await receptionStore.updateReception(idReception, {
...payload
})
if (updated) {
await router.push(`/reception/update/${updated.id}`)
}
router.push("/reception/finish-reception")
return
}
}
</script> </script>

View File

@@ -28,9 +28,9 @@
<div>{{ shipment.customer?.name }}</div> <div>{{ shipment.customer?.name }}</div>
<div>{{ shipment.address?.fullAddress }}</div> <div>{{ shipment.address?.fullAddress }}</div>
<div> <div>
<template v-if="formatBovinShipmentLines(shipment).length"> <template v-if="formatShipmentLines(shipment).length">
<div <div
v-for="(line, index) in formatBovinShipmentLines(shipment)" v-for="(line, index) in formatShipmentLines(shipment)"
:key="index" :key="index"
class="leading-5" class="leading-5"
> >
@@ -62,16 +62,17 @@ const formatWeighing = (shipment: ShipmentData) => {
return `${gross - tare} kg` return `${gross - tare} kg`
} }
const formatBovinShipmentLines = (shipment: ShipmentData) => {
if (!shipment.bovinShipments?.length) { const formatShipmentLines = (shipment: ShipmentData) => {
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
return [] return []
} }
return shipment.bovinShipments.map((entry) => {
const label = typeof entry.shipmentType === 'string' const label = typeof shipment.shipmentType === 'string'
? entry.shipmentType ? shipment.shipmentType
: entry.shipmentType?.label : shipment.shipmentType?.label
return `${label ?? ''} : ${entry.nbBovinSend ?? ''}`
}) return [`${label ?? ''} : ${shipment.nbBovinSend ?? ''}`]
} }
const goShipment = (id: number) => { const goShipment = (id: number) => {

View File

@@ -24,12 +24,12 @@
@click="goToShipment(shipment.id)" @click="goToShipment(shipment.id)"
@keydown.enter="goToShipment(shipment.id)" @keydown.enter="goToShipment(shipment.id)"
> >
<div>{{ shipment.customer?.label }}</div> <div>{{ shipment.customer?.name }}</div>
<div>{{ shipment.address?.fullAddress }}</div> <div>{{ shipment.address?.fullAddress }}</div>
<div> <div>
<template v-if="formatBovinShipmentLines(shipment).length"> <template v-if="formatShipmentLines(shipment).length">
<div <div
v-for="(line, index) in formatBovinShipmentLines(shipment)" v-for="(line, index) in formatShipmentLines(shipment)"
:key="index" :key="index"
class="leading-5" class="leading-5"
> >
@@ -55,16 +55,17 @@ const router = useRouter()
const goToShipment = (id: number) => { const goToShipment = (id: number) => {
router.push(`/shipment/${id}`) router.push(`/shipment/${id}`)
} }
const formatBovinShipmentLines = (shipment: ShipmentData) => {
if (!shipment.bovinShipments?.length) { const formatShipmentLines = (shipment: ShipmentData) => {
if (!shipment.shipmentType && shipment.nbBovinSend == null) {
return [] return []
} }
return shipment.bovinShipments.map((entry) => {
const label = typeof entry.shipmentType === 'string' const label = typeof shipment.shipmentType === 'string'
? entry.shipmentType ? shipment.shipmentType
: entry.shipmentType?.label : shipment.shipmentType?.label
return `${label ?? ''} : ${entry.nbBovinSend ?? ''}`
}) return [`${label ?? ''} : ${shipment.nbBovinSend ?? ''}`]
} }
onMounted(async () => { onMounted(async () => {

View File

@@ -1,50 +0,0 @@
import { useApi } from '~/composables/useApi'
import type { BovinShipmentData } from '~/services/dto/bovin-shipment-data'
import type { ShipmentBovinePayload, BovinShipmentListResponse } from '~/services/dto/bovin-shipment-data'
export async function getBovinShipmentList(
shipmentIri: string
): Promise<BovinShipmentData[]> {
const api = useApi()
const response = await api.get<BovinShipmentListResponse>(
'bovin_shipments',
{ shipment: shipmentIri },
{
toastErrorKey: 'errors.shipmentBovine.list'
}
)
if (Array.isArray(response)) {
return response
}
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
return response['hydra:member']
}
return []
}
export async function createShipmentBovine(
payload: ShipmentBovinePayload
): Promise<BovinShipmentData> {
const api = useApi()
return api.post<BovinShipmentData>('bovin_shipments', payload, {
toastErrorKey: 'errors.shipmentBovine.create'
})
}
export async function deleteShipmentBovine(id: number): Promise<void> {
const api = useApi()
await api.delete<void>(`bovin_shipments/${id}`, {}, {
toastErrorKey: 'errors.shipmentBovine.delete'
})
}
export async function updateShipmentBovine(
id: number,
payload: Partial<ShipmentBovinePayload>
): Promise<BovinShipmentData> {
const api = useApi()
return api.patch<BovinShipmentData>(`bovin_shipments/${id}`, payload, {
toastErrorKey: 'errors.shipmentBovine.update'
})
}

View File

@@ -1,5 +1,5 @@
import { useApi } from '~/composables/useApi' import { useApi } from '~/composables/useApi'
import type {BovineTypeData} from "~/services/dto/bovine-type-data"; import type { BovineTypeData, BovinPayload } from "~/services/dto/bovine-type-data";
export type BovineTypeListResponse = export type BovineTypeListResponse =
| BovineTypeData[] | BovineTypeData[]
@@ -12,12 +12,41 @@ export async function getBovineTypeList(): Promise<BovineTypeData[]> {
}) })
if (Array.isArray(response)) { if (Array.isArray(response)) {
return response return response.map(mapToBovineTypeData)
} }
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
return response['hydra:member'] return response['hydra:member'].map(mapToBovineTypeData)
} }
return [] return []
} }
export async function getBovin(id: number): Promise<BovineTypeData> {
const api = useApi()
const response = await api.get<BovineTypeData>(`bovine_types/${id}`)
return mapToBovineTypeData(response)
}
export async function createBovin(payload: BovinPayload = {}): Promise<BovineTypeData> {
const api = useApi()
const response = await api.post<BovineTypeData>('bovine_types', toBovineTypePayload(payload))
return mapToBovineTypeData(response)
}
export async function updateBovin(id: number, payload: BovinPayload = {}): Promise<BovineTypeData> {
const api = useApi()
const response = await api.patch<BovineTypeData>(`bovine_types/${id}`, toBovineTypePayload(payload))
return mapToBovineTypeData(response)
}
const mapToBovineTypeData = (item: BovineTypeData): BovineTypeData => ({
id: item.id,
label: item.label,
code: item.code
})
const toBovineTypePayload = (payload: BovinPayload): Partial<BovineTypeData> => ({
label: payload.label ?? undefined,
code: payload.code ?? undefined
})

View File

@@ -1,18 +0,0 @@
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
export interface BovinShipmentData {
id: number
nbBovinSend: number | null
shipment?: string | null
shipmentType?: ShipmentTypeData | null
}
export type ShipmentBovinePayload = {
nbBovinSend: number
shipment: string
shipmentType: string
}
export type BovinShipmentListResponse =
| BovinShipmentData[]
| { 'hydra:member'?: BovinShipmentData[] }

View File

@@ -3,3 +3,13 @@ export interface BovineTypeData{
label: string label: string
code: string code: string
} }
export interface BovinFormData {
label: string
code: string
}
export type BovinPayload = {
label?: string | null
code?: string | null
}

View File

@@ -41,9 +41,18 @@ export interface WeightEntryData {
weighedAt: string | null weighedAt: string | null
} }
export interface MerchandiseEntryData {
merchandiseTypeId: string
merchandiseDetail: string
selectedBuildingIds: string[]
selectedPelletBuildingIds: Record<string, string[]>
}
export interface WeightFormData { export interface WeightFormData {
id: number id: number
weight: number weight: number
weighedAt : string
dsd: number
type: 'gross' | 'tare' type: 'gross' | 'tare'
} }
@@ -69,6 +78,7 @@ export type ReceptionPayload = {
} }
export type ReceptionFormData = { export type ReceptionFormData = {
identificationNumber?: null|string,
licensePlate: string licensePlate: string
receptionDate: string receptionDate: string
receptionTypeId: string receptionTypeId: string
@@ -79,6 +89,7 @@ export type ReceptionFormData = {
carrierId: string carrierId: string
driverId: string driverId: string
vehicleId: string vehicleId: string
weight?: ReceptionFormWeight | null
} }
export type ReceptionFormWeight = { export type ReceptionFormWeight = {

View File

@@ -9,12 +9,6 @@ export interface ShipmentTypeData {
code: string code: string
} }
export interface BovinShipmentData {
id?: number
shipmentType?: ShipmentTypeData | string | null
nbBovinSend: number | null
}
export type ShipmentData = { export type ShipmentData = {
id: number id: number
identificationNumber?: string | null identificationNumber?: string | null
@@ -26,7 +20,8 @@ export type ShipmentData = {
carrier?: CarrierData | null carrier?: CarrierData | null
truck?: TruckData | null truck?: TruckData | null
customer?: CustomerData | null customer?: CustomerData | null
bovinShipments?: BovinShipmentData[] | null shipmentType?: ShipmentTypeData | null
nbBovinSend?: number | null
weights?: WeightShipmentEntryData[] | null weights?: WeightShipmentEntryData[] | null
} }
@@ -59,9 +54,9 @@ export type ShipmentPayload = {
carrier?: string | null carrier?: string | null
truck?: string | null truck?: string | null
customer?: string | null customer?: string | null
bovinShipments?: string[] | null
address?: string | null address?: string | null
user?: string | null user?: string | null
driver?: string | null driver?: string | null
shipmentType?: string | null
nbBovinSend?: number | null
} }

View File

@@ -8,8 +8,11 @@ export default <Partial<Config>>{
}, },
colors: { colors: {
primary: { primary: {
700: '#35453C',
500: '#456452', 500: '#456452',
}
}
} }
} }
} }

View File

@@ -0,0 +1,52 @@
<?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 Version20260218144828 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 bovin_shipment DROP CONSTRAINT fk_7049f4502ee48a36');
$this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT fk_7049f4507be036fc');
$this->addSql('DROP TABLE bovin_shipment');
$this->addSql('ALTER TABLE shipment ADD nb_bovin_send INT NOT NULL');
$this->addSql('ALTER TABLE shipment ADD shipment_type_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC2EE48A36 FOREIGN KEY (shipment_type_id) REFERENCES shipment_type (id) NOT DEFERRABLE');
$this->addSql('CREATE INDEX IDX_2CB20DC2EE48A36 ON shipment (shipment_type_id)');
$this->addSql('DROP INDEX uniq_weight_shipment_type');
$this->addSql('DROP INDEX uniq_weight_reception_type');
$this->addSql('ALTER INDEX idx_weight_shipment RENAME TO IDX_7CD55417BE036FC');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE bovin_shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, nb_bovin_send INT NOT NULL, shipment_id INT DEFAULT NULL, shipment_type_id INT DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX idx_7049f4507be036fc ON bovin_shipment (shipment_id)');
$this->addSql('CREATE INDEX idx_7049f4502ee48a36 ON bovin_shipment (shipment_type_id)');
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment_one_type ON bovin_shipment (shipment_id)');
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT fk_7049f4502ee48a36 FOREIGN KEY (shipment_type_id) REFERENCES shipment_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT fk_7049f4507be036fc FOREIGN KEY (shipment_id) REFERENCES shipment (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC2EE48A36');
$this->addSql('DROP INDEX IDX_2CB20DC2EE48A36');
$this->addSql('ALTER TABLE shipment DROP nb_bovin_send');
$this->addSql('ALTER TABLE shipment DROP shipment_type_id');
$this->addSql('CREATE UNIQUE INDEX uniq_weight_shipment_type ON weight (shipment_id, type)');
$this->addSql('CREATE UNIQUE INDEX uniq_weight_reception_type ON weight (reception_id, type)');
$this->addSql('ALTER INDEX idx_7cd55417be036fc RENAME TO idx_weight_shipment');
}
}

View File

@@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity]
#[ApiFilter(SearchFilter::class, properties: ['shipment' => 'exact'])]
#[ORM\UniqueConstraint(name: 'uniq_bovin_shipment_one_type', columns: ['shipment_id'])]
#[ORM\Table(name: 'bovin_shipment')]
#[ApiResource(
operations: [
new Get(
requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['shipment-bovine:read']],
),
new GetCollection(
normalizationContext: ['groups' => ['shipment-bovine:read']],
),
new Post(
normalizationContext: ['groups' => ['shipment-bovine:read']],
denormalizationContext: ['groups' => ['shipment-bovine:write']],
),
new Patch(
normalizationContext: ['groups' => ['shipment-bovine:read']],
denormalizationContext: ['groups' => ['shipment-bovine:write']],
),
new Delete(),
],
security: "is_granted('ROLE_USER')",
)]
class BovinShipment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['shipment:read', 'shipment-bovine:read'])]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'bovinShipments')]
#[Groups(['shipment-bovine:read', 'shipment-bovine:write'])]
#[ApiProperty(readableLink: true)]
private ?Shipment $shipment = null;
#[ORM\ManyToOne]
#[Groups(['shipment:read', 'shipment-bovine:write', 'shipment-bovine:read'])]
#[ApiProperty(readableLink: true)]
private ?ShipmentType $shipmentType = null;
#[ORM\Column]
#[Groups(['shipment:read', 'shipment-bovine:write', 'shipment-bovine:read'])]
private ?int $nbBovinSend = null;
public function getId(): ?int
{
return $this->id;
}
public function getShipment(): ?Shipment
{
return $this->shipment;
}
public function setShipment(?Shipment $shipment): void
{
$this->shipment = $shipment;
}
public function getShipmentType(): ?ShipmentType
{
return $this->shipmentType;
}
public function setShipmentType(?ShipmentType $shipmentType): void
{
$this->shipmentType = $shipmentType;
}
public function getNbBovinSend(): ?int
{
return $this->nbBovinSend;
}
public function setNbBovinSend(?int $nbBovinSend): void
{
$this->nbBovinSend = $nbBovinSend;
}
}

View File

@@ -7,6 +7,8 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\Groups;
@@ -20,6 +22,17 @@ use Symfony\Component\Serializer\Attribute\Groups;
new GetCollection( new GetCollection(
normalizationContext: ['groups' => ['bovine-type:read']], normalizationContext: ['groups' => ['bovine-type:read']],
), ),
new Post(
normalizationContext: ['groups' => ['bovine-type:read']],
denormalizationContext: ['groups' => ['bovine-type:write']],
security: "is_granted('ROLE_ADMIN')",
),
new Patch(
requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['bovine-type:read']],
denormalizationContext: ['groups' => ['bovine-type:write']],
security: "is_granted('ROLE_ADMIN')",
),
], ],
security: "is_granted('ROLE_USER')", security: "is_granted('ROLE_USER')",
)] )]
@@ -32,11 +45,11 @@ class BovineType
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 120)] #[ORM\Column(length: 120)]
#[Groups(['bovine-type:read', 'reception:read', 'reception-bovine:read'])] #[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read'])]
private ?string $label = null; private ?string $label = null;
#[ORM\Column(length: 50)] #[ORM\Column(length: 50)]
#[Groups(['bovine-type:read', 'reception:read', 'reception-bovine:read'])] #[Groups(['bovine-type:read', 'bovine-type:write', 'reception:read', 'reception-bovine:read'])]
private ?string $code = null; private ?string $code = null;
public function getId(): ?int public function getId(): ?int

View File

@@ -123,17 +123,15 @@ class Shipment
#[ApiProperty(readableLink: true)] #[ApiProperty(readableLink: true)]
private ?Customer $customer = null; private ?Customer $customer = null;
/** #[ORM\ManyToOne(inversedBy: 'shipments')]
* @var Collection<int, BovinShipment> #[ORM\JoinColumn(nullable: true)]
*/
#[ORM\OneToMany(
targetEntity: BovinShipment::class,
mappedBy: 'shipment',
cascade: ['persist', 'remove'],
orphanRemoval: true
)]
#[Groups(['shipment:read', 'shipment:write'])] #[Groups(['shipment:read', 'shipment:write'])]
private Collection $bovinShipments; #[ApiProperty(readableLink: true)]
private ?ShipmentType $shipmentType = null;
#[ORM\Column]
#[Groups(['shipment:read', 'shipment:write'])]
private int $nbBovinSend = 0;
/** /**
* @var Collection<int, Weight> * @var Collection<int, Weight>
@@ -156,7 +154,6 @@ class Shipment
public function __construct() public function __construct()
{ {
$this->bovinShipments = new ArrayCollection();
$this->weights = new ArrayCollection(); $this->weights = new ArrayCollection();
} }
@@ -195,6 +192,26 @@ class Shipment
$this->currentStep = $currentStep; $this->currentStep = $currentStep;
} }
public function getShipmentType(): ?ShipmentType
{
return $this->shipmentType;
}
public function setShipmentType(?ShipmentType $shipmentType): void
{
$this->shipmentType = $shipmentType;
}
public function getNbBovinSend(): int
{
return $this->nbBovinSend;
}
public function setNbBovinSend(int $nbBovinSend): void
{
$this->nbBovinSend = $nbBovinSend;
}
public function getIsValid(): ?bool public function getIsValid(): ?bool
{ {
return $this->isValid; return $this->isValid;
@@ -261,37 +278,6 @@ class Shipment
$this->customer = $customer; $this->customer = $customer;
} }
public function getBovinShipments(): Collection
{
return $this->bovinShipments;
}
public function setBovinShipments(Collection $bovinShipments): void
{
$this->bovinShipments = $bovinShipments;
}
public function addBovinShipment(BovinShipment $bovinShipment): self
{
if (!$this->bovinShipments->contains($bovinShipment)) {
$this->bovinShipments->add($bovinShipment);
$bovinShipment->setShipment($this);
}
return $this;
}
public function removeBovinShipment(BovinShipment $bovinShipment): self
{
if ($this->bovinShipments->removeElement($bovinShipment)) {
if ($bovinShipment->getShipment() === $this) {
$bovinShipment->setShipment(null);
}
}
return $this;
}
/** /**
* @return Collection<int, Weight> * @return Collection<int, Weight>
*/ */

View File

@@ -7,6 +7,8 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GetCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\Groups;
@@ -40,6 +42,14 @@ class ShipmentType
#[Groups(['shipment-type:read', 'shipment:read'])] #[Groups(['shipment-type:read', 'shipment:read'])]
private ?string $code = null; private ?string $code = null;
#[ORM\OneToMany(mappedBy: 'shipmentType', targetEntity: Shipment::class)]
private Collection $shipments;
public function __construct()
{
$this->shipments = new ArrayCollection();
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;

View File

@@ -26,7 +26,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
), ),
new GetCollection( new GetCollection(
normalizationContext: ['groups' => ['supplier:read']], normalizationContext: ['groups' => ['supplier:read']],
security: "is_granted('ROLE_ADMIN')" security: "is_granted('ROLE_USER')"
), ),
new Post( new Post(
normalizationContext: ['groups' => ['supplier:read']], normalizationContext: ['groups' => ['supplier:read']],

View File

@@ -220,9 +220,15 @@
<tr class="border-bottom"> <tr class="border-bottom">
<td> <td>
{% if reception.receptionType and reception.receptionType.code == 'BOVINS' %}
<strong> <strong>
Type de bovins Type de bovins
</strong> </strong>
{% else %}
<strong>
Type de marchandises
</strong>
{% endif %}
<br><br> <br><br>
<div class="bigtable-notes"> <div class="bigtable-notes">
@@ -263,6 +269,9 @@
<p>Aucun dépôt de granulés renseigné.</p> <p>Aucun dépôt de granulés renseigné.</p>
{% endfor %} {% endfor %}
{% else %} {% else %}
{% if reception.merchandiseType %}
<p><strong>{{ reception.merchandiseType.label }}</strong></p>
{% endif %}
{% set buildingLabels = [] %} {% set buildingLabels = [] %}
{% for building in reception.buildings|default([]) %} {% for building in reception.buildings|default([]) %}
{% set buildingLabels = buildingLabels|merge([building.label]) %} {% set buildingLabels = buildingLabels|merge([building.label]) %}

View File

@@ -250,13 +250,11 @@
<td> <td>
<strong>Bovin</strong><br><br> <strong>Bovin</strong><br><br>
<div class="bigtable-notes"> <div class="bigtable-notes">
{% if shipment.bovinShipments is not empty %} {% if shipment.shipmentType %}
{% for entry in shipment.bovinShipments %}
<p> <p>
{{ entry.shipmentType ? entry.shipmentType.label : '-' }} : {{ shipment.shipmentType.label ?? '-' }} :
{{ entry.nbBovinSend ?? 0 }} {{ shipment.nbBovinSend ?? 0 }}
</p> </p>
{% endfor %}
{% else %} {% else %}
<p>-</p> <p>-</p>
{% endif %} {% endif %}