feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception
This commit is contained in:
232
frontend/components/reception/reception-product-received.vue
Normal file
232
frontend/components/reception/reception-product-received.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-16">
|
||||
<!-- @TODO voir pour séparer dans un composant au moment de l'implémentation des Bovins -->
|
||||
<div
|
||||
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
|
||||
class="flex flex-col gap-8 items-center w-full">
|
||||
<h1 class="text-4xl uppercase font-bold">Sélectionner des marchandises réceptionnnées</h1>
|
||||
<div class="flex flex-col w-[550px]">
|
||||
<label for="merchandise-type" class="font-bold uppercase text-xl mb-2">Type de marchandises</label>
|
||||
<select
|
||||
id="merchandise-type"
|
||||
v-model="selectedMerchandiseTypeId"
|
||||
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent cursor-pointer"
|
||||
:class="selectedMerchandiseTypeId ? 'text-black' : 'text-neutral-400'"
|
||||
>
|
||||
<option value="" disabled class="text-neutral-400">Sélectionner</option>
|
||||
<option
|
||||
v-for="type in merchandiseTypes"
|
||||
:key="type.id"
|
||||
:value="String(type.id)"
|
||||
class="text-black"
|
||||
>
|
||||
{{ type.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && isAutres"
|
||||
class="flex flex-col w-full max-w-[550px]"
|
||||
>
|
||||
<label for="merchandise-detail" class="font-bold uppercase text-xl mb-2">Préciser</label>
|
||||
<input
|
||||
id="merchandise-detail"
|
||||
v-model="merchandiseDetail"
|
||||
placeholder="Précisions complémentaires"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
class="border-b border-black text-xl pb-[6px] bg-transparent"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && !isGranule"
|
||||
class="flex gap-4 w-[550px] justify-evenly"
|
||||
>
|
||||
<div
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
>
|
||||
<label
|
||||
class="flex items-center gap-2 text-xl"
|
||||
>
|
||||
<input
|
||||
v-model="selectedBuildingIds"
|
||||
type="checkbox"
|
||||
:value="String(building.id)"
|
||||
>
|
||||
<span>{{ building.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedMerchandiseTypeId && isGranule"
|
||||
class="flex flex-col gap-10 w-full max-w-[900px]"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
|
||||
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
|
||||
<p class="font-bold uppercase">{{ type.label }}</p>
|
||||
<label
|
||||
v-for="building in buildings"
|
||||
:key="building.id"
|
||||
class="flex items-center gap-2 text-lg"
|
||||
>
|
||||
<input
|
||||
v-model="selectedPelletBuildingIds[String(type.id)]"
|
||||
type="checkbox"
|
||||
:value="String(building.id)"
|
||||
>
|
||||
<span>{{ building.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
@click="goNext"
|
||||
>Peser</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getBuildingList } from '~/services/building'
|
||||
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 {
|
||||
createReceptionPelletBuilding,
|
||||
deleteReceptionPelletBuilding,
|
||||
getReceptionPelletBuildingList
|
||||
} from '~/services/reception-pellet-building'
|
||||
import { useReceptionStore } from '~/stores/reception'
|
||||
import { MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES } from '~/utils/constants'
|
||||
|
||||
const receptionStore = useReceptionStore()
|
||||
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
|
||||
const buildings = ref<BuildingData[]>([])
|
||||
const pelletTypes = ref<PelletTypeData[]>([])
|
||||
const selectedMerchandiseTypeId = ref('')
|
||||
const selectedBuildingIds = ref<string[]>([])
|
||||
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||
const merchandiseDetail = ref('')
|
||||
|
||||
const selectedMerchandiseType = computed(() =>
|
||||
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value)
|
||||
)
|
||||
const isGranule = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE)
|
||||
const isAutres = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES)
|
||||
|
||||
onMounted(async () => {
|
||||
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
||||
getMerchandiseTypeList(),
|
||||
getBuildingList(),
|
||||
getPelletTypeList()
|
||||
])
|
||||
merchandiseTypes.value = merchandiseTypeList
|
||||
buildings.value = buildingList
|
||||
pelletTypes.value = pelletTypeList
|
||||
|
||||
const currentId = receptionStore.current?.merchandiseType?.id
|
||||
if (currentId) {
|
||||
selectedMerchandiseTypeId.value = String(currentId)
|
||||
}
|
||||
merchandiseDetail.value = receptionStore.current?.merchandiseDetail ?? ''
|
||||
|
||||
selectedBuildingIds.value =
|
||||
receptionStore.current?.buildings?.map((building) => String(building.id)) ?? []
|
||||
|
||||
const existingPelletSelections = receptionStore.current?.pelletBuildings ?? []
|
||||
const selectionMap: Record<string, string[]> = {}
|
||||
for (const selection of existingPelletSelections) {
|
||||
const pelletTypeId = String(selection.pelletType.id)
|
||||
const buildingId = String(selection.building.id)
|
||||
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
|
||||
})
|
||||
|
||||
async function goNext() {
|
||||
if (!receptionStore.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
const receptionIri = `/api/receptions/${receptionStore.current.id}`
|
||||
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
merchandiseType: selectedMerchandiseTypeId.value
|
||||
? `/api/merchandise_types/${selectedMerchandiseTypeId.value}`
|
||||
: null,
|
||||
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() : null,
|
||||
buildings: isGranule.value
|
||||
? []
|
||||
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
|
||||
currentStep: nextStep
|
||||
})
|
||||
|
||||
if (isGranule.value) {
|
||||
await syncPelletSelections(receptionIri)
|
||||
} else {
|
||||
await clearPelletSelections(receptionIri)
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPelletSelections(receptionIri: string) {
|
||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||
for (const selection of existing) {
|
||||
await deleteReceptionPelletBuilding(selection.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPelletSelections(receptionIri: string) {
|
||||
const existing = await getReceptionPelletBuildingList(receptionIri)
|
||||
const existingMap = new Map<string, number>()
|
||||
for (const selection of existing) {
|
||||
const key = `${selection.pelletType.id}:${selection.building.id}`
|
||||
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>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center mt-[164px] gap-32">
|
||||
<div class="flex gap-8 items-center justify-center">
|
||||
<!--@TODO Prendre en compte que l'on peut aussi décharger de la marchandise-->
|
||||
<h1 class="text-4xl uppercase font-bold">Décharger les bêtes</h1>
|
||||
<UiLoadingDots />
|
||||
</div>
|
||||
<button
|
||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||
@click="goNext"
|
||||
>Peser</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useReceptionStore } from '~/stores/reception'
|
||||
|
||||
const receptionStore = useReceptionStore()
|
||||
|
||||
async function goNext() {
|
||||
if (!receptionStore.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = receptionStore.current.currentStep + 1
|
||||
await receptionStore.updateReception(receptionStore.current.id, {
|
||||
currentStep: nextStep
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -6,21 +6,26 @@ export const usePdfPrinter = () => {
|
||||
const currentReception = receptionStore.current
|
||||
|
||||
const printPdf = async (url: string): Promise<void> => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
const blob = await api.getBlob(url);
|
||||
|
||||
const pdfBlob = blob.type === 'application/pdf'
|
||||
? blob
|
||||
: new Blob([blob], { type: 'application/pdf' });
|
||||
|
||||
const blobUrl = URL.createObjectURL(pdfBlob);
|
||||
|
||||
const filename = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}.pdf`;
|
||||
|
||||
const blob = await api.getBlob(url)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
// nom du fichier à changer par les infos du store pinia
|
||||
a.download = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}`;
|
||||
a.href = blobUrl;
|
||||
a.download = filename;
|
||||
a.style.display = 'none';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.open(blobUrl, '_blank', 'noopener,noreferrer')
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60000)
|
||||
|
||||
window.open(blobUrl, '_blank', 'noopener,noreferrer');
|
||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,6 +17,20 @@
|
||||
"receptionType": {
|
||||
"list": "Impossible de récupérer la liste des types de réception."
|
||||
},
|
||||
"merchandiseType": {
|
||||
"list": "Impossible de récupérer la liste des types de marchandises."
|
||||
},
|
||||
"building": {
|
||||
"list": "Impossible de récupérer la liste des bâtiments."
|
||||
},
|
||||
"pelletType": {
|
||||
"list": "Impossible de récupérer la liste des types de granulés."
|
||||
},
|
||||
"receptionPelletBuilding": {
|
||||
"list": "Impossible de récupérer la liste des dépôts de granulés.",
|
||||
"create": "Impossible d'enregistrer le dépôt de granulés.",
|
||||
"delete": "Impossible de supprimer le dépôt de granulés."
|
||||
},
|
||||
"supplier": {
|
||||
"list": "Impossible de récupérer la liste des fournisseurs."
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
|
||||
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
|
||||
<ReceptionUnloading v-if="storeReception?.currentStep === 2"/>
|
||||
<ReceptionProductReceived v-if="storeReception?.currentStep === 2"/>
|
||||
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
23
frontend/services/building.ts
Normal file
23
frontend/services/building.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
|
||||
export type BuildingListResponse =
|
||||
| BuildingData[]
|
||||
| { 'hydra:member'?: BuildingData[] }
|
||||
|
||||
export async function getBuildingList(): Promise<BuildingData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<BuildingListResponse>('buildings', {}, {
|
||||
toastErrorKey: 'errors.building.list'
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
5
frontend/services/dto/building-data.ts
Normal file
5
frontend/services/dto/building-data.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface BuildingData {
|
||||
id: number
|
||||
label: string
|
||||
code: string
|
||||
}
|
||||
5
frontend/services/dto/merchandise-type-data.ts
Normal file
5
frontend/services/dto/merchandise-type-data.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface MerchandiseTypeData {
|
||||
id: number
|
||||
label: string
|
||||
code: string
|
||||
}
|
||||
5
frontend/services/dto/pellet-type-data.ts
Normal file
5
frontend/services/dto/pellet-type-data.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface PelletTypeData {
|
||||
id: number
|
||||
label: string
|
||||
code: string
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
|
||||
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
import type { ReceptionPelletBuildingData } from '~/services/dto/reception-pellet-building-data'
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import type { SupplierData } from '~/services/dto/supplier-data'
|
||||
import type { AddressData } from '~/services/dto/address-data'
|
||||
@@ -15,6 +18,10 @@ export interface ReceptionData {
|
||||
currentStep: number
|
||||
isValid: boolean
|
||||
receptionType?: ReceptionTypeData | null
|
||||
merchandiseType?: MerchandiseTypeData | null
|
||||
merchandiseDetail?: string | null
|
||||
buildings?: BuildingData[] | null
|
||||
pelletBuildings?: ReceptionPelletBuildingData[] | null
|
||||
user?: UserData | null
|
||||
supplier?: SupplierData | null
|
||||
address?: AddressData | null
|
||||
@@ -37,6 +44,9 @@ export type ReceptionPayload = {
|
||||
currentStep?: number
|
||||
isValid?: boolean
|
||||
receptionType?: string | null
|
||||
merchandiseType?: string | null
|
||||
merchandiseDetail?: string | null
|
||||
buildings?: string[] | null
|
||||
user?: string | null
|
||||
supplier?: string | null
|
||||
address?: string | null
|
||||
|
||||
9
frontend/services/dto/reception-pellet-building-data.ts
Normal file
9
frontend/services/dto/reception-pellet-building-data.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { BuildingData } from '~/services/dto/building-data'
|
||||
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
||||
|
||||
export interface ReceptionPelletBuildingData {
|
||||
id: number
|
||||
reception?: string
|
||||
building: BuildingData
|
||||
pelletType: PelletTypeData
|
||||
}
|
||||
23
frontend/services/merchandise-type.ts
Normal file
23
frontend/services/merchandise-type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
|
||||
|
||||
export type MerchandiseTypeListResponse =
|
||||
| MerchandiseTypeData[]
|
||||
| { 'hydra:member'?: MerchandiseTypeData[] }
|
||||
|
||||
export async function getMerchandiseTypeList(): Promise<MerchandiseTypeData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<MerchandiseTypeListResponse>('merchandise_types', {}, {
|
||||
toastErrorKey: 'errors.merchandiseType.list'
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
23
frontend/services/pellet-type.ts
Normal file
23
frontend/services/pellet-type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
|
||||
|
||||
export type PelletTypeListResponse =
|
||||
| PelletTypeData[]
|
||||
| { 'hydra:member'?: PelletTypeData[] }
|
||||
|
||||
export async function getPelletTypeList(): Promise<PelletTypeData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<PelletTypeListResponse>('pellet_types', {}, {
|
||||
toastErrorKey: 'errors.pelletType.list'
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response
|
||||
}
|
||||
|
||||
if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
51
frontend/services/reception-pellet-building.ts
Normal file
51
frontend/services/reception-pellet-building.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { ReceptionPelletBuildingData } from '~/services/dto/reception-pellet-building-data'
|
||||
|
||||
export type ReceptionPelletBuildingListResponse =
|
||||
| ReceptionPelletBuildingData[]
|
||||
| { 'hydra:member'?: ReceptionPelletBuildingData[] }
|
||||
|
||||
export type ReceptionPelletBuildingPayload = {
|
||||
reception: string
|
||||
pelletType: string
|
||||
building: string
|
||||
}
|
||||
|
||||
export async function getReceptionPelletBuildingList(
|
||||
receptionIri: string
|
||||
): Promise<ReceptionPelletBuildingData[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<ReceptionPelletBuildingListResponse>(
|
||||
'reception_pellet_buildings',
|
||||
{ reception: receptionIri },
|
||||
{
|
||||
toastErrorKey: 'errors.receptionPelletBuilding.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 createReceptionPelletBuilding(
|
||||
payload: ReceptionPelletBuildingPayload
|
||||
): Promise<ReceptionPelletBuildingData> {
|
||||
const api = useApi()
|
||||
return api.post<ReceptionPelletBuildingData>('reception_pellet_buildings', payload, {
|
||||
toastErrorKey: 'errors.receptionPelletBuilding.create'
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteReceptionPelletBuilding(id: number): Promise<void> {
|
||||
const api = useApi()
|
||||
await api.delete<void>(`reception_pellet_buildings/${id}`, {}, {
|
||||
toastErrorKey: 'errors.receptionPelletBuilding.delete'
|
||||
})
|
||||
}
|
||||
8
frontend/utils/constants.ts
Normal file
8
frontend/utils/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const RECEPTION_TYPE_CODES = {
|
||||
MERCHANDISES: 'MARCHANDISES'
|
||||
} as const
|
||||
|
||||
export const MERCHANDISE_TYPE_CODES = {
|
||||
GRANULE: 'GRANULE',
|
||||
AUTRES: 'AUTRES'
|
||||
} as const
|
||||
Reference in New Issue
Block a user