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

This commit is contained in:
AUTIN Tristan
2026-01-15 18:37:44 +01:00
parent 4a77449a41
commit 94ea49587a
22 changed files with 1153 additions and 220 deletions
+65 -12
View File
@@ -4,22 +4,72 @@ import { $fetch, FetchError } from 'ofetch'
export type AnyObject = Record<string, unknown>
export type ApiClient = {
get<T>(url: string, query?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
post<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
put<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
patch<T>(url: string, body?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
delete<T>(url: string, query?: AnyObject, options?: FetchOptions<'json'>): Promise<T>
get<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
getBlob(url: string, query?: AnyObject, options?: ApiFetchOptions<'blob'>): Promise<Blob>
post<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
put<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
patch<T>(url: string, body?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
delete<T>(url: string, query?: AnyObject, options?: ApiFetchOptions<'json'>): Promise<T>
}
export type ApiFetchOptions<ResponseType extends 'json' | 'blob'> =
FetchOptions<ResponseType> & {
toast?: boolean
toastTitle?: string
}
export const useApi = (): ApiClient => {
const config = useRuntimeConfig()
const baseURL = config.public.apiBase ?? '/api'
const client = $fetch.create({ baseURL, retry: 0 })
const toast = useToast()
const extractErrorMessage = (error: unknown, responseData?: unknown): string => {
const data = responseData ?? (error as FetchError)?.data
if (typeof data === 'string') {
return data
}
if (data && typeof data === 'object') {
const record = data as Record<string, unknown>
return (
(record['hydra:description'] as string) ||
(record.detail as string) ||
(record.message as string) ||
(record.error as string) ||
(record.title as string) ||
(record['hydra:title'] as string) ||
''
)
}
return (error as FetchError)?.message ?? 'Erreur inconnue.'
}
const client = $fetch.create({
baseURL,
retry: 0,
onResponseError({ response, error, options }) {
const apiOptions = options as ApiFetchOptions<'json'>
if (apiOptions?.toast === false) {
return
}
const message =
extractErrorMessage(error, response?._data) ||
'Une erreur est survenue.'
toast.error({
title: apiOptions?.toastTitle ?? 'Erreur',
message
})
}
})
const request = <T>(
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
url: string,
options: FetchOptions<'json'> = {}
options: ApiFetchOptions<'json'> = {}
) => {
const needsJsonBody = method === 'POST' || method === 'PUT'
const needsMergePatch = method === 'PATCH'
@@ -36,19 +86,22 @@ export const useApi = (): ApiClient => {
}
return {
get<T>(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) {
get<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('GET', url, { ...options, query })
},
post<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
getBlob(url: string, query: AnyObject = {}, options: ApiFetchOptions<'blob'> = {}) {
return client<Blob>(url, { ...options, method: 'GET', query, responseType: 'blob' })
},
post<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('POST', url, { ...options, body })
},
put<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
put<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('PUT', url, { ...options, body })
},
patch<T>(url: string, body: AnyObject = {}, options: FetchOptions<'json'> = {}) {
patch<T>(url: string, body: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('PATCH', url, { ...options, body })
},
delete<T>(url: string, query: AnyObject = {}, options: FetchOptions<'json'> = {}) {
delete<T>(url: string, query: AnyObject = {}, options: ApiFetchOptions<'json'> = {}) {
return request<T>('DELETE', url, { ...options, query })
}
}
+40
View File
@@ -0,0 +1,40 @@
import type { Ref } from 'vue'
import { useApi } from '~/composables/useApi'
type PrintFrameRef = Ref<HTMLIFrameElement | null>
export const usePdfPrinter = () => {
const api = useApi()
const printPdf = async (url: string, frameRef: PrintFrameRef): Promise<void> => {
if (!import.meta.client) {
return
}
const frame = frameRef.value
if (!frame) {
return
}
// On charge le PDF en blob pour rester en same-origin dans l'iframe.
const blob = await api.getBlob(url)
const blobUrl = URL.createObjectURL(blob)
const tryPrint = () => {
frame.contentWindow?.focus()
frame.contentWindow?.print()
}
frame.onload = () => {
tryPrint()
// On libere l'URL blob apres l'impression.
setTimeout(() => URL.revokeObjectURL(blobUrl), 2000)
}
frame.src = blobUrl
setTimeout(tryPrint, 1200)
}
return {
printPdf
}
}
+34 -44
View File
@@ -1,9 +1,9 @@
import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import type { ReceptionData, WeightEntryData } from '~/services/dto/reception-data'
import type { WeightData } from '~/services/dto/weight-data'
import { getWeight } from '~/services/reception'
import { createWeight, updateWeight } from '~/services/weight'
import type {Ref} from 'vue'
import {computed, ref} from 'vue'
import type {ReceptionData, WeightEntryData} from '~/services/dto/reception-data'
import type {WeightData} from '~/services/dto/weight-data'
import {getWeight} from '~/services/reception'
import {createWeight, updateWeight} from '~/services/weight'
export type WeighingMode = 'gross' | 'tare'
@@ -12,18 +12,16 @@ type UseWeighingOptions = {
reception: Ref<ReceptionData | null>
updateReception: (id: number, payload: Partial<ReceptionData>) => Promise<ReceptionData | null>
loadReception?: (id: number) => Promise<ReceptionData | null>
storeError?: Ref<string | null>
}
export const useWeighing = ({
mode,
reception,
updateReception,
loadReception,
storeError
loadReception
}: UseWeighingOptions) => {
const weightData = ref<WeightData | null>(null)
const localErrorMessage = ref<string | null>(null)
const isFetching = ref(false)
const currentWeightEntry = computed<WeightEntryData | null>(() => {
const weights = reception.value?.weights ?? []
@@ -33,22 +31,19 @@ export const useWeighing = ({
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
const errorMessage = computed(() => localErrorMessage.value ?? storeError?.value ?? null)
const showLoadingBox = computed(() => displayWeight.value === null && !errorMessage.value)
const showLoadingBox = computed(
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
)
const fetchWeight = async () => {
localErrorMessage.value = null
try {
weightData.value = await getWeight()
} catch (error) {
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
}
isFetching.value = true
weightData.value = await getWeight().finally(() => {
isFetching.value = false
})
}
const saveWeight = async () => {
localErrorMessage.value = null
if (!reception.value) {
localErrorMessage.value = 'Réception introuvable.'
return
}
@@ -58,36 +53,32 @@ export const useWeighing = ({
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
if (baseWeight === null) {
localErrorMessage.value = 'Veuillez dabord peser.'
return
}
try {
if (existingEntry?.id) {
await updateWeight(existingEntry.id, {
type: mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
} else {
await createWeight({
reception: `/receptions/${reception.value.id}`,
type: mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
}
} catch (error) {
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
return
if (existingEntry?.id) {
await updateWeight(existingEntry.id, {
type: mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
} else {
await createWeight({
reception: `/receptions/${reception.value.id}`,
type: mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
}
const nextStep = reception.value.currentStep + 1
const nextStep = mode === 'tare'
? reception.value.currentStep
: reception.value.currentStep + 1
await updateReception(reception.value.id, {
currentStep: nextStep,
isValid: mode === 'tare' ? true : reception.value.isValid
isValid: reception.value.isValid
})
if (loadReception) {
@@ -101,7 +92,6 @@ export const useWeighing = ({
displayWeight,
displayDsd,
title,
errorMessage,
showLoadingBox,
fetchWeight,
saveWeight