refactor(front): extract shared utils and rewire pages
This commit is contained in:
@@ -246,6 +246,16 @@ const emitSelection = (ids: string[]) => {
|
||||
emit('update:modelValue', normalized)
|
||||
}
|
||||
|
||||
const extractDataArray = (data: unknown): ConstructeurSummary[] => {
|
||||
if (Array.isArray(data)) {
|
||||
return data as ConstructeurSummary[]
|
||||
}
|
||||
if (data && typeof data === 'object' && 'id' in data) {
|
||||
return [data as ConstructeurSummary]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const ensureOptionsLoaded = async (force = false) => {
|
||||
if (!force && !searchTerm.value && constructeurs.value.length) {
|
||||
applyOptions(constructeurs.value as ConstructeurSummary[])
|
||||
@@ -262,7 +272,7 @@ const ensureOptionsLoaded = async (force = false) => {
|
||||
|
||||
const result = await searchConstructeurs(searchTerm.value)
|
||||
if (result.success) {
|
||||
applyOptions(result.data || [])
|
||||
applyOptions(extractDataArray(result.data))
|
||||
lastSearchTerm = searchTerm.value
|
||||
}
|
||||
}
|
||||
@@ -283,7 +293,7 @@ const onSearch = () => {
|
||||
}
|
||||
const result = await searchConstructeurs(searchTerm.value)
|
||||
if (result.success) {
|
||||
applyOptions(result.data || [])
|
||||
applyOptions(extractDataArray(result.data))
|
||||
lastSearchTerm = searchTerm.value
|
||||
}
|
||||
}, 250)
|
||||
@@ -310,16 +320,18 @@ const closeCreateModal = () => {
|
||||
|
||||
const handleCreate = async () => {
|
||||
creating.value = true
|
||||
const payload = { ...createForm.value }
|
||||
if (!payload.phone) {
|
||||
delete payload.phone
|
||||
const payload: { name: string; email?: string; phone?: string } = {
|
||||
name: createForm.value.name,
|
||||
}
|
||||
if (!payload.email) {
|
||||
delete payload.email
|
||||
if (createForm.value.email) {
|
||||
payload.email = createForm.value.email
|
||||
}
|
||||
if (createForm.value.phone) {
|
||||
payload.phone = createForm.value.phone
|
||||
}
|
||||
const result = await createConstructeur(payload)
|
||||
creating.value = false
|
||||
if (result.success) {
|
||||
if (result.success && result.data && !Array.isArray(result.data)) {
|
||||
emitSelection([...selectedIds.value, result.data.id])
|
||||
searchTerm.value = ''
|
||||
closeCreateModal()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="space-y-1">
|
||||
<SearchSelect
|
||||
:model-value="modelValue"
|
||||
:model-value="modelValue ?? undefined"
|
||||
:options="productOptions"
|
||||
:loading="loading"
|
||||
:placeholder="placeholder"
|
||||
|
||||
@@ -825,10 +825,9 @@ const customFieldReorderClass = (index: number) => {
|
||||
|
||||
const addCustomField = () => {
|
||||
ensureArray('customFields')
|
||||
const nextIndex = Array.isArray(props.node.customFields)
|
||||
? props.node.customFields.length
|
||||
: 0
|
||||
props.node.customFields.push({
|
||||
const fields = props.node.customFields!
|
||||
const nextIndex = fields.length
|
||||
fields.push({
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
@@ -847,7 +846,7 @@ const removeCustomField = (index: number) => {
|
||||
|
||||
const addPiece = () => {
|
||||
ensureArray('pieces')
|
||||
props.node.pieces.push({
|
||||
props.node.pieces!.push({
|
||||
typePieceId: '',
|
||||
typePieceLabel: '',
|
||||
reference: '',
|
||||
@@ -863,7 +862,7 @@ const removePiece = (index: number) => {
|
||||
|
||||
const addProduct = () => {
|
||||
ensureArray('products')
|
||||
props.node.products.push({
|
||||
props.node.products!.push({
|
||||
typeProductId: '',
|
||||
typeProductLabel: '',
|
||||
familyCode: '',
|
||||
@@ -911,6 +910,7 @@ const moveItemInPlace = <T,>(list: T[], from: number, to: number) => {
|
||||
}
|
||||
const updated = list.slice()
|
||||
const [item] = updated.splice(from, 1)
|
||||
if (item === undefined) return
|
||||
updated.splice(to, 0, item)
|
||||
list.splice(0, list.length, ...updated)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
:placeholder="labels.labelPlaceholder"
|
||||
@input="updateRequirement(index, { label: $event.target.value })"
|
||||
@input="handleLabelInput(index, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
type="number"
|
||||
min="0"
|
||||
class="input input-bordered input-sm"
|
||||
@input="updateRequirement(index, { minCount: parseNumber($event.target.value) })"
|
||||
@input="handleMinInput(index, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
type="number"
|
||||
min="0"
|
||||
class="input input-bordered input-sm"
|
||||
@input="updateRequirement(index, { maxCount: parseOptionalNumber($event.target.value) })"
|
||||
@input="handleMaxInput(index, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
:checked="(requirement.required ?? requiredFallback) === true"
|
||||
@change="updateRequirement(index, { required: $event.target.checked })"
|
||||
@change="handleRequiredChange(index, $event)"
|
||||
/>
|
||||
{{ labels.requiredLabel }}
|
||||
</label>
|
||||
@@ -123,7 +123,7 @@
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
:checked="(requirement.allowNewModels ?? allowNewModelsFallback) === true"
|
||||
@change="updateRequirement(index, { allowNewModels: $event.target.checked })"
|
||||
@change="handleAllowNewModelsChange(index, $event)"
|
||||
/>
|
||||
{{ labels.allowNewModelsLabel }}
|
||||
</label>
|
||||
@@ -277,6 +277,37 @@ const parseOptionalNumber = (value: string) => {
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
// Type-safe event handlers
|
||||
const getInputValue = (event: Event): string => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
return target?.value ?? ''
|
||||
}
|
||||
|
||||
const getCheckboxValue = (event: Event): boolean => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
return target?.checked ?? false
|
||||
}
|
||||
|
||||
const handleLabelInput = (index: number, event: Event) => {
|
||||
updateRequirement(index, { label: getInputValue(event) })
|
||||
}
|
||||
|
||||
const handleMinInput = (index: number, event: Event) => {
|
||||
updateRequirement(index, { minCount: parseNumber(getInputValue(event)) })
|
||||
}
|
||||
|
||||
const handleMaxInput = (index: number, event: Event) => {
|
||||
updateRequirement(index, { maxCount: parseOptionalNumber(getInputValue(event)) })
|
||||
}
|
||||
|
||||
const handleRequiredChange = (index: number, event: Event) => {
|
||||
updateRequirement(index, { required: getCheckboxValue(event) })
|
||||
}
|
||||
|
||||
const handleAllowNewModelsChange = (index: number, event: Event) => {
|
||||
updateRequirement(index, { allowNewModels: getCheckboxValue(event) })
|
||||
}
|
||||
|
||||
const draggingRequirementIndex = ref<number | null>(null)
|
||||
const requirementDropTargetIndex = ref<number | null>(null)
|
||||
|
||||
@@ -297,6 +328,10 @@ const reorderRequirements = (from: number, to: number) => {
|
||||
}
|
||||
const updated = list.slice() as Requirement[]
|
||||
const [moved] = updated.splice(from, 1)
|
||||
if (!moved) {
|
||||
resetRequirementDragState()
|
||||
return
|
||||
}
|
||||
updated.splice(to, 0, moved)
|
||||
requirements.value = applyOrderIndex(updated)
|
||||
resetRequirementDragState()
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { listModelTypes, createModelType, updateModelType, deleteModelType } from '~/services/modelTypes'
|
||||
import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes'
|
||||
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||
|
||||
const componentTypes = ref([])
|
||||
export interface ComponentType extends ModelType {
|
||||
structure: ComponentModelStructure | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
interface ComponentTypePayload {
|
||||
name: string
|
||||
code?: string
|
||||
description?: string | null
|
||||
notes?: string | null
|
||||
structure?: ComponentModelStructure | null
|
||||
}
|
||||
|
||||
interface ComponentTypeResult {
|
||||
success: boolean
|
||||
data?: ComponentType | ComponentType[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
const componentTypes = ref<ComponentType[]>([])
|
||||
const loadingComponentTypes = ref(false)
|
||||
|
||||
export function useComponentTypes () {
|
||||
export function useComponentTypes() {
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const generateCodeFromName = (name) => {
|
||||
const generateCodeFromName = (name: string): string => {
|
||||
return (name || '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036F]/g, '')
|
||||
@@ -18,24 +38,26 @@ export function useComponentTypes () {
|
||||
.replace(/-+/g, '-') || 'type'
|
||||
}
|
||||
|
||||
const loadComponentTypes = async () => {
|
||||
const loadComponentTypes = async (): Promise<ComponentTypeResult> => {
|
||||
loadingComponentTypes.value = true
|
||||
try {
|
||||
const data = await listModelTypes({
|
||||
category: 'COMPONENT',
|
||||
sort: 'name',
|
||||
dir: 'asc',
|
||||
limit: 200
|
||||
limit: 200,
|
||||
})
|
||||
|
||||
componentTypes.value = data.items.map(item => ({
|
||||
componentTypes.value = data.items.map((item) => ({
|
||||
...item,
|
||||
description: item.description ?? item.notes ?? null
|
||||
structure: item.structure as ComponentModelStructure | null,
|
||||
description: item.description ?? item.notes ?? null,
|
||||
}))
|
||||
|
||||
return { success: true, data: componentTypes.value }
|
||||
} catch (error) {
|
||||
const message = error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { message?: string }
|
||||
const message = err?.message || 'Erreur inconnue'
|
||||
showError(`Impossible de charger les types de composant: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -43,21 +65,22 @@ export function useComponentTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const createComponentType = async (payload) => {
|
||||
const createComponentType = async (payload: ComponentTypePayload): Promise<ComponentTypeResult> => {
|
||||
loadingComponentTypes.value = true
|
||||
try {
|
||||
const data = await createModelType({
|
||||
name: payload.name,
|
||||
code: payload.code || generateCodeFromName(payload.name),
|
||||
category: 'COMPONENT',
|
||||
notes: payload.description ?? payload.notes,
|
||||
description: payload.description ?? null,
|
||||
structure: payload.structure
|
||||
notes: payload.description ?? payload.notes ?? undefined,
|
||||
description: payload.description ?? undefined,
|
||||
structure: payload.structure ?? undefined,
|
||||
})
|
||||
|
||||
const normalized = {
|
||||
const normalized: ComponentType = {
|
||||
...data,
|
||||
description: data.description ?? data.notes ?? null
|
||||
structure: data.structure as ComponentModelStructure | null,
|
||||
description: data.description ?? data.notes ?? null,
|
||||
}
|
||||
|
||||
componentTypes.value.push(normalized)
|
||||
@@ -65,7 +88,8 @@ export function useComponentTypes () {
|
||||
|
||||
return { success: true, data: normalized }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la création du type de composant: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -73,34 +97,33 @@ export function useComponentTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const updateComponentType = async (id, payload) => {
|
||||
const updateComponentType = async (id: string, payload: ComponentTypePayload): Promise<ComponentTypeResult> => {
|
||||
loadingComponentTypes.value = true
|
||||
try {
|
||||
const data = await updateModelType(id, {
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
notes: payload.notes,
|
||||
description: payload.description ?? undefined,
|
||||
notes: payload.notes ?? undefined,
|
||||
code: payload.code,
|
||||
structure: payload.structure
|
||||
structure: payload.structure ?? undefined,
|
||||
})
|
||||
|
||||
const normalized = {
|
||||
const normalized: ComponentType = {
|
||||
...data,
|
||||
description: data.description ?? data.notes ?? null
|
||||
structure: data.structure as ComponentModelStructure | null,
|
||||
description: data.description ?? data.notes ?? null,
|
||||
}
|
||||
|
||||
const index = componentTypes.value.findIndex(type => type.id === id)
|
||||
const index = componentTypes.value.findIndex((type) => type.id === id)
|
||||
if (index !== -1) {
|
||||
componentTypes.value[index] = normalized
|
||||
}
|
||||
showSuccess(`Type de composant "${data.name}" mis à jour`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: normalized
|
||||
}
|
||||
return { success: true, data: normalized }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la mise à jour du type de composant: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -108,15 +131,16 @@ export function useComponentTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteComponentType = async (id) => {
|
||||
const deleteComponentType = async (id: string): Promise<ComponentTypeResult> => {
|
||||
loadingComponentTypes.value = true
|
||||
try {
|
||||
await deleteModelType(id)
|
||||
componentTypes.value = componentTypes.value.filter(type => type.id !== id)
|
||||
componentTypes.value = componentTypes.value.filter((type) => type.id !== id)
|
||||
showSuccess('Type de composant supprimé')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la suppression du type de composant: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -135,6 +159,6 @@ export function useComponentTypes () {
|
||||
updateComponentType,
|
||||
deleteComponentType,
|
||||
getComponentTypes,
|
||||
isComponentTypeLoading
|
||||
isComponentTypeLoading,
|
||||
}
|
||||
}
|
||||
@@ -2,45 +2,83 @@ import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { useApi } from './useApi'
|
||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import { useConstructeurs } from './useConstructeurs'
|
||||
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||
|
||||
const composants = ref([])
|
||||
export interface Composant {
|
||||
id: string
|
||||
name: string
|
||||
reference?: string | null
|
||||
typeComposantId?: string | null
|
||||
typeComposant?: { id: string; name?: string } | null
|
||||
productId?: string | null
|
||||
product?: { id: string; name?: string } | null
|
||||
constructeurs?: Constructeur[]
|
||||
constructeurIds?: string[]
|
||||
documents?: unknown[]
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ComposantListResult {
|
||||
success: boolean
|
||||
data?: { items: Composant[]; total: number; page: number; itemsPerPage: number }
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ComposantSingleResult {
|
||||
success: boolean
|
||||
data?: Composant
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface LoadComposantsOptions {
|
||||
search?: string
|
||||
page?: number
|
||||
itemsPerPage?: number
|
||||
orderBy?: string
|
||||
orderDir?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
const composants = ref<Composant[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const extractCollection = (payload) => {
|
||||
const extractCollection = (payload: unknown): Composant[] => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
return payload as Composant[]
|
||||
}
|
||||
if (Array.isArray(payload?.member)) {
|
||||
return payload.member
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (Array.isArray(p?.member)) {
|
||||
return p.member as Composant[]
|
||||
}
|
||||
if (Array.isArray(payload?.['hydra:member'])) {
|
||||
return payload['hydra:member']
|
||||
if (Array.isArray(p?.['hydra:member'])) {
|
||||
return p['hydra:member'] as Composant[]
|
||||
}
|
||||
if (Array.isArray(payload?.data)) {
|
||||
return payload.data
|
||||
if (Array.isArray(p?.data)) {
|
||||
return p.data as Composant[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const extractTotal = (payload, fallbackLength) => {
|
||||
if (typeof payload?.totalItems === 'number') {
|
||||
return payload.totalItems
|
||||
const extractTotal = (payload: unknown, fallbackLength: number): number => {
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (typeof p?.totalItems === 'number') {
|
||||
return p.totalItems
|
||||
}
|
||||
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||
return payload['hydra:totalItems']
|
||||
if (typeof p?.['hydra:totalItems'] === 'number') {
|
||||
return p['hydra:totalItems']
|
||||
}
|
||||
return fallbackLength
|
||||
}
|
||||
|
||||
export function useComposants () {
|
||||
const { showSuccess, showError, showInfo } = useToast()
|
||||
export function useComposants() {
|
||||
const { showSuccess } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
const { ensureConstructeurs } = useConstructeurs()
|
||||
|
||||
const withResolvedConstructeurs = async (composant) => {
|
||||
const withResolvedConstructeurs = async (composant: Composant): Promise<Composant> => {
|
||||
if (!composant || typeof composant !== 'object') {
|
||||
return composant
|
||||
}
|
||||
@@ -59,12 +97,11 @@ export function useComposants () {
|
||||
const ids = uniqueConstructeurIds(
|
||||
composant.constructeurIds,
|
||||
composant.constructeurs,
|
||||
composant.constructeur,
|
||||
)
|
||||
const hasResolvedConstructeurs =
|
||||
Array.isArray(composant.constructeurs)
|
||||
&& composant.constructeurs.length > 0
|
||||
&& composant.constructeurs.every((item) => item && typeof item === 'object')
|
||||
Array.isArray(composant.constructeurs) &&
|
||||
composant.constructeurs.length > 0 &&
|
||||
composant.constructeurs.every((item) => item && typeof item === 'object')
|
||||
|
||||
if (ids.length && !hasResolvedConstructeurs) {
|
||||
const resolved = await ensureConstructeurs(ids)
|
||||
@@ -76,16 +113,7 @@ export function useComposants () {
|
||||
return composant
|
||||
}
|
||||
|
||||
/**
|
||||
* Load composants with pagination and search support
|
||||
* @param {Object} options - Query options
|
||||
* @param {string} [options.search] - Search term for name/reference
|
||||
* @param {number} [options.page=1] - Current page (1-based)
|
||||
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||
* @param {string} [options.orderBy='name'] - Field to order by
|
||||
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||
*/
|
||||
const loadComposants = async (options = {}) => {
|
||||
const loadComposants = async (options: LoadComposantsOptions = {}): Promise<ComposantListResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const {
|
||||
@@ -93,7 +121,7 @@ export function useComposants () {
|
||||
page = 1,
|
||||
itemsPerPage = 30,
|
||||
orderBy = 'name',
|
||||
orderDir = 'asc'
|
||||
orderDir = 'asc',
|
||||
} = options
|
||||
|
||||
const params = new URLSearchParams()
|
||||
@@ -118,79 +146,84 @@ export function useComposants () {
|
||||
items: enrichedItems,
|
||||
total: total.value,
|
||||
page,
|
||||
itemsPerPage
|
||||
}
|
||||
itemsPerPage,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result as ComposantListResult
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des composants:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createComposant = async (composantData) => {
|
||||
const createComposant = async (composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
||||
const result = await post('/composants', normalizedPayload)
|
||||
if (result.success) {
|
||||
const enriched = await withResolvedConstructeurs(result.data)
|
||||
if (result.success && result.data) {
|
||||
const enriched = await withResolvedConstructeurs(result.data as Composant)
|
||||
composants.value.unshift(enriched)
|
||||
total.value += 1
|
||||
const displayName = result.data?.name
|
||||
|| composantData?.definition?.name
|
||||
|| composantData?.name
|
||||
|| 'Composant'
|
||||
const definition = (composantData as Record<string, unknown>)?.definition as Record<string, unknown> | undefined
|
||||
const displayName =
|
||||
(result.data as Composant)?.name ||
|
||||
(definition?.name as string | undefined) ||
|
||||
composantData?.name ||
|
||||
'Composant'
|
||||
showSuccess(`Composant "${displayName}" créé avec succès`)
|
||||
return { success: true, data: enriched }
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du composant:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateComposantData = async (id, composantData) => {
|
||||
const updateComposantData = async (id: string, composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
||||
const result = await patch(`/composants/${id}`, normalizedPayload)
|
||||
if (result.success) {
|
||||
const updated = await withResolvedConstructeurs(result.data)
|
||||
const index = composants.value.findIndex(comp => comp.id === id)
|
||||
if (result.success && result.data) {
|
||||
const updated = await withResolvedConstructeurs(result.data as Composant)
|
||||
const index = composants.value.findIndex((comp) => comp.id === id)
|
||||
if (index !== -1) {
|
||||
composants.value[index] = updated
|
||||
}
|
||||
showSuccess(`Composant "${updated?.name || composantData.name || ''}" mis à jour avec succès`)
|
||||
return { success: true, data: updated }
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour du composant:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteComposant = async (id) => {
|
||||
const deleteComposant = async (id: string): Promise<ComposantSingleResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await del(`/composants/${id}`)
|
||||
if (result.success) {
|
||||
const deletedComposant = composants.value.find(comp => comp.id === id)
|
||||
composants.value = composants.value.filter(comp => comp.id !== id)
|
||||
const deletedComposant = composants.value.find((comp) => comp.id === id)
|
||||
composants.value = composants.value.filter((comp) => comp.id !== id)
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
|
||||
return { success: true }
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression du composant:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -208,6 +241,6 @@ export function useComposants () {
|
||||
updateComposant: updateComposantData,
|
||||
deleteComposant,
|
||||
getComposants,
|
||||
isLoading
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,24 @@ import { ref } from 'vue'
|
||||
import { useApi } from './useApi'
|
||||
import { useToast } from './useToast'
|
||||
|
||||
const constructeurs = ref([])
|
||||
export interface Constructeur {
|
||||
id: string
|
||||
name: string
|
||||
email?: string | null
|
||||
phone?: string | null
|
||||
}
|
||||
|
||||
interface ConstructeurResult {
|
||||
success: boolean
|
||||
data?: Constructeur | Constructeur[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
const constructeurs = ref<Constructeur[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const uniqueConstructeurs = (items = []) => {
|
||||
const map = new Map()
|
||||
const uniqueConstructeurs = (items: Constructeur[] = []): Constructeur[] => {
|
||||
const map = new Map<string, Constructeur>()
|
||||
items.forEach((item) => {
|
||||
if (item && typeof item === 'object' && typeof item.id === 'string') {
|
||||
map.set(item.id, item)
|
||||
@@ -15,7 +28,7 @@ const uniqueConstructeurs = (items = []) => {
|
||||
return Array.from(map.values())
|
||||
}
|
||||
|
||||
const normalizeIds = (ids = []) => {
|
||||
const normalizeIds = (ids: unknown[] = []): string[] => {
|
||||
if (!Array.isArray(ids)) {
|
||||
return []
|
||||
}
|
||||
@@ -28,7 +41,7 @@ const normalizeIds = (ids = []) => {
|
||||
)
|
||||
}
|
||||
|
||||
const upsertConstructeurs = (items = []) => {
|
||||
const upsertConstructeurs = (items: Constructeur[] = []) => {
|
||||
if (!Array.isArray(items) || !items.length) {
|
||||
return
|
||||
}
|
||||
@@ -36,32 +49,33 @@ const upsertConstructeurs = (items = []) => {
|
||||
constructeurs.value = merged
|
||||
}
|
||||
|
||||
const getIndexedConstructeur = (id) =>
|
||||
const getIndexedConstructeur = (id: string): Constructeur | null =>
|
||||
constructeurs.value.find((item) => item && item.id === id) || null
|
||||
|
||||
const extractCollection = (payload) => {
|
||||
const extractCollection = (payload: unknown): Constructeur[] => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
return payload as Constructeur[]
|
||||
}
|
||||
if (Array.isArray(payload?.member)) {
|
||||
return payload.member
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (Array.isArray(p?.member)) {
|
||||
return p.member as Constructeur[]
|
||||
}
|
||||
if (Array.isArray(payload?.['hydra:member'])) {
|
||||
return payload['hydra:member']
|
||||
if (Array.isArray(p?.['hydra:member'])) {
|
||||
return p['hydra:member'] as Constructeur[]
|
||||
}
|
||||
if (Array.isArray(payload?.data)) {
|
||||
return payload.data
|
||||
if (Array.isArray(p?.data)) {
|
||||
return p.data as Constructeur[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const pendingFetches = new Map()
|
||||
const pendingFetches = new Map<string, Promise<Constructeur | null>>()
|
||||
|
||||
export function useConstructeurs () {
|
||||
export function useConstructeurs() {
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const loadConstructeurs = async (search = '') => {
|
||||
const loadConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
||||
@@ -70,47 +84,49 @@ export function useConstructeurs () {
|
||||
const items = extractCollection(result.data)
|
||||
constructeurs.value = uniqueConstructeurs(items)
|
||||
}
|
||||
return result
|
||||
return result as ConstructeurResult
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
console.error('Erreur lors du chargement des fournisseurs:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchConstructeurs = async (search = '') => {
|
||||
const searchConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
|
||||
return loadConstructeurs(search)
|
||||
}
|
||||
|
||||
const createConstructeur = async (data) => {
|
||||
const createConstructeur = async (data: Partial<Constructeur>): Promise<ConstructeurResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await post('/constructeurs', data)
|
||||
if (result.success) {
|
||||
upsertConstructeurs([result.data])
|
||||
showSuccess(`Fournisseur "${result.data.name}" créé`)
|
||||
upsertConstructeurs([result.data as Constructeur])
|
||||
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" créé`)
|
||||
} else if (result.error) {
|
||||
showError(result.error)
|
||||
}
|
||||
return result
|
||||
return result as ConstructeurResult
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
console.error('Erreur lors de la création du fournisseur:', error)
|
||||
showError('Impossible de créer le fournisseur')
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const ensureConstructeurs = async (ids = []) => {
|
||||
const ensureConstructeurs = async (ids: unknown[] = []): Promise<Constructeur[]> => {
|
||||
const normalizedIds = normalizeIds(ids)
|
||||
if (!normalizedIds.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const collected = []
|
||||
const missing = []
|
||||
const collected: Constructeur[] = []
|
||||
const missing: string[] = []
|
||||
normalizedIds.forEach((id) => {
|
||||
const existing = getIndexedConstructeur(id)
|
||||
if (existing) {
|
||||
@@ -129,7 +145,7 @@ export function useConstructeurs () {
|
||||
const task = get(`/constructeurs/${id}`)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
return result.data
|
||||
return result.data as Constructeur
|
||||
}
|
||||
return null
|
||||
})
|
||||
@@ -145,7 +161,7 @@ export function useConstructeurs () {
|
||||
})
|
||||
|
||||
const fetched = await Promise.all(fetchTasks)
|
||||
const validFetched = fetched.filter((item) => item && item.id)
|
||||
const validFetched = fetched.filter((item): item is Constructeur => item !== null && item.id !== undefined)
|
||||
if (validFetched.length) {
|
||||
upsertConstructeurs(validFetched)
|
||||
}
|
||||
@@ -153,50 +169,52 @@ export function useConstructeurs () {
|
||||
|
||||
return normalizedIds
|
||||
.map((id) => getIndexedConstructeur(id))
|
||||
.filter((item) => Boolean(item))
|
||||
.filter((item): item is Constructeur => item !== null)
|
||||
}
|
||||
|
||||
const updateConstructeur = async (id, data) => {
|
||||
const updateConstructeur = async (id: string, data: Partial<Constructeur>): Promise<ConstructeurResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await patch(`/constructeurs/${id}`, data)
|
||||
if (result.success) {
|
||||
upsertConstructeurs([result.data])
|
||||
showSuccess(`Fournisseur "${result.data.name}" mis à jour`)
|
||||
upsertConstructeurs([result.data as Constructeur])
|
||||
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" mis à jour`)
|
||||
} else if (result.error) {
|
||||
showError(result.error)
|
||||
}
|
||||
return result
|
||||
return result as ConstructeurResult
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
console.error('Erreur lors de la mise à jour du fournisseur:', error)
|
||||
showError('Impossible de mettre à jour le fournisseur')
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteConstructeur = async (id) => {
|
||||
const deleteConstructeur = async (id: string): Promise<ConstructeurResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await del(`/constructeurs/${id}`)
|
||||
if (result.success) {
|
||||
constructeurs.value = constructeurs.value.filter(item => item.id !== id)
|
||||
constructeurs.value = constructeurs.value.filter((item) => item.id !== id)
|
||||
showSuccess('Fournisseur supprimé')
|
||||
} else if (result.error) {
|
||||
showError(result.error)
|
||||
}
|
||||
return result
|
||||
return result as ConstructeurResult
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
console.error('Erreur lors de la suppression du fournisseur:', error)
|
||||
showError('Impossible de supprimer le fournisseur')
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getConstructeurById = (id) => getIndexedConstructeur(id)
|
||||
const getConstructeurById = (id: string) => getIndexedConstructeur(id)
|
||||
|
||||
return {
|
||||
constructeurs,
|
||||
@@ -1,169 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
import { useApi } from './useApi'
|
||||
import { useToast } from './useToast'
|
||||
import { normalizeRelationIds } from '~/shared/apiRelations'
|
||||
|
||||
const documents = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const extractCollection = (payload) => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
if (Array.isArray(payload?.member)) {
|
||||
return payload.member
|
||||
}
|
||||
if (Array.isArray(payload?.['hydra:member'])) {
|
||||
return payload['hydra:member']
|
||||
}
|
||||
if (Array.isArray(payload?.data)) {
|
||||
return payload.data
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const fileToBase64 = file =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = () => reject(new Error(`Lecture du fichier ${file.name} impossible`))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
|
||||
export function useDocuments () {
|
||||
const { get, post, delete: del } = useApi()
|
||||
const { showError, showSuccess } = useToast()
|
||||
|
||||
const loadFromEndpoint = async (endpoint, { updateStore = false } = {}) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await get(endpoint)
|
||||
if (result.success) {
|
||||
const data = extractCollection(result.data)
|
||||
if (updateStore) {
|
||||
documents.value = data
|
||||
}
|
||||
return { success: true, data }
|
||||
}
|
||||
if (result.error) {
|
||||
showError(result.error)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors du chargement des documents (${endpoint}):`, error)
|
||||
showError('Impossible de charger les documents')
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadDocuments = async (options = {}) => {
|
||||
return loadFromEndpoint('/documents', { updateStore: options.updateStore ?? true })
|
||||
}
|
||||
|
||||
const loadDocumentsBySite = async (siteId, options = {}) => {
|
||||
if (!siteId) { return { success: false, error: 'Aucun site sélectionné' } }
|
||||
return loadFromEndpoint(`/documents/site/${siteId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByMachine = async (machineId, options = {}) => {
|
||||
if (!machineId) { return { success: false, error: 'Aucune machine sélectionnée' } }
|
||||
return loadFromEndpoint(`/documents/machine/${machineId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByComponent = async (componentId, options = {}) => {
|
||||
if (!componentId) { return { success: false, error: 'Aucun composant sélectionné' } }
|
||||
return loadFromEndpoint(`/documents/composant/${componentId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByProduct = async (productId, options = {}) => {
|
||||
if (!productId) { return { success: false, error: 'Aucun produit sélectionné' } }
|
||||
return loadFromEndpoint(`/documents/product/${productId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByPiece = async (pieceId, options = {}) => {
|
||||
if (!pieceId) { return { success: false, error: 'Aucune pièce sélectionnée' } }
|
||||
return loadFromEndpoint(`/documents/piece/${pieceId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const uploadDocuments = async ({ files = [], context = {} }, { updateStore = false } = {}) => {
|
||||
if (!files.length) { return { success: false, error: 'Aucun fichier sélectionné' } }
|
||||
|
||||
loading.value = true
|
||||
const created = []
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
const dataUrl = await fileToBase64(file)
|
||||
|
||||
const payload = normalizeRelationIds({
|
||||
name: file.name,
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
size: file.size,
|
||||
path: dataUrl,
|
||||
...context
|
||||
})
|
||||
|
||||
const result = await post('/documents', payload)
|
||||
if (result.success) {
|
||||
created.push(result.data)
|
||||
showSuccess(`Document "${file.name}" ajouté`)
|
||||
} else if (result.error) {
|
||||
showError(`Erreur sur ${file.name} : ${result.error}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (created.length) {
|
||||
if (updateStore) {
|
||||
documents.value = [...created, ...documents.value]
|
||||
}
|
||||
return { success: true, data: created }
|
||||
}
|
||||
|
||||
return { success: false, error: 'Aucun document ajouté' }
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'upload des documents:', error)
|
||||
showError("Échec de l'ajout des documents")
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteDocument = async (id, { updateStore = false } = {}) => {
|
||||
if (!id) { return { success: false, error: 'Identifiant manquant' } }
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await del(`/documents/${id}`)
|
||||
if (result.success) {
|
||||
if (updateStore) {
|
||||
documents.value = documents.value.filter(doc => doc.id !== id)
|
||||
}
|
||||
showSuccess('Document supprimé')
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression du document:', error)
|
||||
showError('Impossible de supprimer le document')
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
documents,
|
||||
loading,
|
||||
loadDocuments,
|
||||
loadDocumentsBySite,
|
||||
loadDocumentsByMachine,
|
||||
loadDocumentsByComponent,
|
||||
loadDocumentsByPiece,
|
||||
loadDocumentsByProduct,
|
||||
uploadDocuments,
|
||||
deleteDocument
|
||||
}
|
||||
}
|
||||
241
app/composables/useDocuments.ts
Normal file
241
app/composables/useDocuments.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { ref } from 'vue'
|
||||
import { useApi } from './useApi'
|
||||
import { useToast } from './useToast'
|
||||
import { normalizeRelationIds } from '~/shared/apiRelations'
|
||||
|
||||
export interface Document {
|
||||
id: string
|
||||
name: string
|
||||
filename: string
|
||||
mimeType: string
|
||||
size: number
|
||||
path: string
|
||||
siteId?: string
|
||||
machineId?: string
|
||||
composantId?: string
|
||||
productId?: string
|
||||
pieceId?: string
|
||||
}
|
||||
|
||||
export interface UploadContext {
|
||||
siteId?: string
|
||||
machineId?: string
|
||||
composantId?: string
|
||||
productId?: string
|
||||
pieceId?: string
|
||||
}
|
||||
|
||||
export interface DocumentResult {
|
||||
success: boolean
|
||||
data?: Document | Document[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
const documents = ref<Document[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const extractCollection = (payload: unknown): Document[] => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (Array.isArray(p?.member)) {
|
||||
return p.member as Document[]
|
||||
}
|
||||
if (Array.isArray(p?.['hydra:member'])) {
|
||||
return p['hydra:member'] as Document[]
|
||||
}
|
||||
if (Array.isArray(p?.data)) {
|
||||
return p.data as Document[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const fileToBase64 = (file: File): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = () => reject(new Error(`Lecture du fichier ${file.name} impossible`))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
|
||||
export function useDocuments() {
|
||||
const { get, post, delete: del } = useApi()
|
||||
const { showError, showSuccess } = useToast()
|
||||
|
||||
const loadFromEndpoint = async (
|
||||
endpoint: string,
|
||||
{ updateStore = false }: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await get(endpoint)
|
||||
if (result.success) {
|
||||
const data = extractCollection(result.data)
|
||||
if (updateStore) {
|
||||
documents.value = data
|
||||
}
|
||||
return { success: true, data }
|
||||
}
|
||||
if (result.error) {
|
||||
showError(result.error)
|
||||
}
|
||||
return result as DocumentResult
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
console.error(`Erreur lors du chargement des documents (${endpoint}):`, error)
|
||||
showError('Impossible de charger les documents')
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadDocuments = async (
|
||||
options: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
return loadFromEndpoint('/documents', { updateStore: options.updateStore ?? true })
|
||||
}
|
||||
|
||||
const loadDocumentsBySite = async (
|
||||
siteId: string,
|
||||
options: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
if (!siteId) {
|
||||
return { success: false, error: 'Aucun site sélectionné' }
|
||||
}
|
||||
return loadFromEndpoint(`/documents/site/${siteId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByMachine = async (
|
||||
machineId: string,
|
||||
options: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
if (!machineId) {
|
||||
return { success: false, error: 'Aucune machine sélectionnée' }
|
||||
}
|
||||
return loadFromEndpoint(`/documents/machine/${machineId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByComponent = async (
|
||||
componentId: string,
|
||||
options: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
if (!componentId) {
|
||||
return { success: false, error: 'Aucun composant sélectionné' }
|
||||
}
|
||||
return loadFromEndpoint(`/documents/composant/${componentId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByProduct = async (
|
||||
productId: string,
|
||||
options: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
if (!productId) {
|
||||
return { success: false, error: 'Aucun produit sélectionné' }
|
||||
}
|
||||
return loadFromEndpoint(`/documents/product/${productId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const loadDocumentsByPiece = async (
|
||||
pieceId: string,
|
||||
options: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
if (!pieceId) {
|
||||
return { success: false, error: 'Aucune pièce sélectionnée' }
|
||||
}
|
||||
return loadFromEndpoint(`/documents/piece/${pieceId}`, { updateStore: options.updateStore ?? false })
|
||||
}
|
||||
|
||||
const uploadDocuments = async (
|
||||
{ files, context = {} }: { files: File[]; context?: UploadContext },
|
||||
{ updateStore = false }: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
if (!files.length) {
|
||||
return { success: false, error: 'Aucun fichier sélectionné' }
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const created: Document[] = []
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
const dataUrl = await fileToBase64(file)
|
||||
|
||||
const payload = normalizeRelationIds({
|
||||
name: file.name,
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
size: file.size,
|
||||
path: dataUrl,
|
||||
...context,
|
||||
})
|
||||
|
||||
const result = await post('/documents', payload)
|
||||
if (result.success) {
|
||||
created.push(result.data as Document)
|
||||
showSuccess(`Document "${file.name}" ajouté`)
|
||||
} else if (result.error) {
|
||||
showError(`Erreur sur ${file.name} : ${result.error}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (created.length) {
|
||||
if (updateStore) {
|
||||
documents.value = [...created, ...documents.value]
|
||||
}
|
||||
return { success: true, data: created }
|
||||
}
|
||||
|
||||
return { success: false, error: 'Aucun document ajouté' }
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
console.error("Erreur lors de l'upload des documents:", error)
|
||||
showError("Échec de l'ajout des documents")
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteDocument = async (
|
||||
id: string | number,
|
||||
{ updateStore = false }: { updateStore?: boolean } = {},
|
||||
): Promise<DocumentResult> => {
|
||||
if (!id) {
|
||||
return { success: false, error: 'Identifiant manquant' }
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await del(`/documents/${id}`)
|
||||
if (result.success) {
|
||||
if (updateStore) {
|
||||
documents.value = documents.value.filter((doc) => doc.id !== id)
|
||||
}
|
||||
showSuccess('Document supprimé')
|
||||
}
|
||||
return result as DocumentResult
|
||||
} catch (error) {
|
||||
const err = error as Error
|
||||
console.error('Erreur lors de la suppression du document:', error)
|
||||
showError('Impossible de supprimer le document')
|
||||
return { success: false, error: err.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
documents,
|
||||
loading,
|
||||
loadDocuments,
|
||||
loadDocumentsBySite,
|
||||
loadDocumentsByMachine,
|
||||
loadDocumentsByComponent,
|
||||
loadDocumentsByPiece,
|
||||
loadDocumentsByProduct,
|
||||
uploadDocuments,
|
||||
deleteDocument,
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,34 @@
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { listModelTypes, createModelType, updateModelType, deleteModelType } from '~/services/modelTypes'
|
||||
import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes'
|
||||
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||
|
||||
const pieceTypes = ref([])
|
||||
export interface PieceType extends ModelType {
|
||||
structure: PieceModelStructure | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
interface PieceTypePayload {
|
||||
name: string
|
||||
code?: string
|
||||
description?: string | null
|
||||
notes?: string | null
|
||||
structure?: PieceModelStructure | null
|
||||
}
|
||||
|
||||
interface PieceTypeResult {
|
||||
success: boolean
|
||||
data?: PieceType | PieceType[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
const pieceTypes = ref<PieceType[]>([])
|
||||
const loadingPieceTypes = ref(false)
|
||||
|
||||
export function usePieceTypes () {
|
||||
export function usePieceTypes() {
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const generateCodeFromName = (name) => {
|
||||
const generateCodeFromName = (name: string): string => {
|
||||
return (name || '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036F]/g, '')
|
||||
@@ -18,24 +38,26 @@ export function usePieceTypes () {
|
||||
.replace(/-+/g, '-') || 'type'
|
||||
}
|
||||
|
||||
const loadPieceTypes = async () => {
|
||||
const loadPieceTypes = async (): Promise<PieceTypeResult> => {
|
||||
loadingPieceTypes.value = true
|
||||
try {
|
||||
const data = await listModelTypes({
|
||||
category: 'PIECE',
|
||||
sort: 'name',
|
||||
dir: 'asc',
|
||||
limit: 200
|
||||
limit: 200,
|
||||
})
|
||||
|
||||
pieceTypes.value = data.items.map(item => ({
|
||||
pieceTypes.value = data.items.map((item) => ({
|
||||
...item,
|
||||
description: item.description ?? item.notes ?? null
|
||||
structure: item.structure as PieceModelStructure | null,
|
||||
description: item.description ?? item.notes ?? null,
|
||||
}))
|
||||
|
||||
return { success: true, data: pieceTypes.value }
|
||||
} catch (error) {
|
||||
const message = error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { message?: string }
|
||||
const message = err?.message || 'Erreur inconnue'
|
||||
showError(`Impossible de charger les types de pièce: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -43,21 +65,22 @@ export function usePieceTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const createPieceType = async (payload) => {
|
||||
const createPieceType = async (payload: PieceTypePayload): Promise<PieceTypeResult> => {
|
||||
loadingPieceTypes.value = true
|
||||
try {
|
||||
const data = await createModelType({
|
||||
name: payload.name,
|
||||
code: payload.code || generateCodeFromName(payload.name),
|
||||
category: 'PIECE',
|
||||
notes: payload.description ?? payload.notes,
|
||||
description: payload.description ?? null,
|
||||
structure: payload.structure
|
||||
notes: payload.description ?? payload.notes ?? undefined,
|
||||
description: payload.description ?? undefined,
|
||||
structure: payload.structure ?? undefined,
|
||||
})
|
||||
|
||||
const normalized = {
|
||||
const normalized: PieceType = {
|
||||
...data,
|
||||
description: data.description ?? data.notes ?? null
|
||||
structure: data.structure as PieceModelStructure | null,
|
||||
description: data.description ?? data.notes ?? null,
|
||||
}
|
||||
|
||||
pieceTypes.value.push(normalized)
|
||||
@@ -65,7 +88,8 @@ export function usePieceTypes () {
|
||||
|
||||
return { success: true, data: normalized }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la création du type de pièce: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -73,34 +97,33 @@ export function usePieceTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const updatePieceType = async (id, payload) => {
|
||||
const updatePieceType = async (id: string, payload: PieceTypePayload): Promise<PieceTypeResult> => {
|
||||
loadingPieceTypes.value = true
|
||||
try {
|
||||
const data = await updateModelType(id, {
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
notes: payload.notes,
|
||||
description: payload.description ?? undefined,
|
||||
notes: payload.notes ?? undefined,
|
||||
code: payload.code,
|
||||
structure: payload.structure
|
||||
structure: payload.structure ?? undefined,
|
||||
})
|
||||
|
||||
const normalized = {
|
||||
const normalized: PieceType = {
|
||||
...data,
|
||||
description: data.description ?? data.notes ?? null
|
||||
structure: data.structure as PieceModelStructure | null,
|
||||
description: data.description ?? data.notes ?? null,
|
||||
}
|
||||
|
||||
const index = pieceTypes.value.findIndex(type => type.id === id)
|
||||
const index = pieceTypes.value.findIndex((type) => type.id === id)
|
||||
if (index !== -1) {
|
||||
pieceTypes.value[index] = normalized
|
||||
}
|
||||
showSuccess(`Type de pièce "${data.name}" mis à jour`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: normalized
|
||||
}
|
||||
return { success: true, data: normalized }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la mise à jour du type de pièce: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -108,15 +131,16 @@ export function usePieceTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const deletePieceType = async (id) => {
|
||||
const deletePieceType = async (id: string): Promise<PieceTypeResult> => {
|
||||
loadingPieceTypes.value = true
|
||||
try {
|
||||
await deleteModelType(id)
|
||||
pieceTypes.value = pieceTypes.value.filter(type => type.id !== id)
|
||||
pieceTypes.value = pieceTypes.value.filter((type) => type.id !== id)
|
||||
showSuccess('Type de pièce supprimé')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la suppression du type de pièce: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -135,6 +159,6 @@ export function usePieceTypes () {
|
||||
updatePieceType,
|
||||
deletePieceType,
|
||||
getPieceTypes,
|
||||
isPieceTypeLoading
|
||||
isPieceTypeLoading,
|
||||
}
|
||||
}
|
||||
@@ -2,45 +2,84 @@ import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { useApi } from './useApi'
|
||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import { useConstructeurs } from './useConstructeurs'
|
||||
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||
|
||||
const pieces = ref([])
|
||||
export interface Piece {
|
||||
id: string
|
||||
name: string
|
||||
reference?: string | null
|
||||
typePieceId?: string | null
|
||||
typePiece?: { id: string; name?: string } | null
|
||||
productId?: string | null
|
||||
productIds?: string[]
|
||||
product?: { id: string; name?: string } | null
|
||||
constructeurs?: Constructeur[]
|
||||
constructeurIds?: string[]
|
||||
documents?: unknown[]
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface PieceListResult {
|
||||
success: boolean
|
||||
data?: { items: Piece[]; total: number; page: number; itemsPerPage: number }
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface PieceSingleResult {
|
||||
success: boolean
|
||||
data?: Piece
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface LoadPiecesOptions {
|
||||
search?: string
|
||||
page?: number
|
||||
itemsPerPage?: number
|
||||
orderBy?: string
|
||||
orderDir?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
const pieces = ref<Piece[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const extractCollection = (payload) => {
|
||||
const extractCollection = (payload: unknown): Piece[] => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
return payload as Piece[]
|
||||
}
|
||||
if (Array.isArray(payload?.member)) {
|
||||
return payload.member
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (Array.isArray(p?.member)) {
|
||||
return p.member as Piece[]
|
||||
}
|
||||
if (Array.isArray(payload?.['hydra:member'])) {
|
||||
return payload['hydra:member']
|
||||
if (Array.isArray(p?.['hydra:member'])) {
|
||||
return p['hydra:member'] as Piece[]
|
||||
}
|
||||
if (Array.isArray(payload?.data)) {
|
||||
return payload.data
|
||||
if (Array.isArray(p?.data)) {
|
||||
return p.data as Piece[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const extractTotal = (payload, fallbackLength) => {
|
||||
if (typeof payload?.totalItems === 'number') {
|
||||
return payload.totalItems
|
||||
const extractTotal = (payload: unknown, fallbackLength: number): number => {
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (typeof p?.totalItems === 'number') {
|
||||
return p.totalItems
|
||||
}
|
||||
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||
return payload['hydra:totalItems']
|
||||
if (typeof p?.['hydra:totalItems'] === 'number') {
|
||||
return p['hydra:totalItems']
|
||||
}
|
||||
return fallbackLength
|
||||
}
|
||||
|
||||
export function usePieces () {
|
||||
const { showSuccess, showError, showInfo } = useToast()
|
||||
export function usePieces() {
|
||||
const { showSuccess } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
const { ensureConstructeurs } = useConstructeurs()
|
||||
|
||||
const withResolvedConstructeurs = async (piece) => {
|
||||
const withResolvedConstructeurs = async (piece: Piece): Promise<Piece> => {
|
||||
if (!piece || typeof piece !== 'object') {
|
||||
return piece
|
||||
}
|
||||
@@ -68,12 +107,11 @@ export function usePieces () {
|
||||
const ids = uniqueConstructeurIds(
|
||||
piece.constructeurIds,
|
||||
piece.constructeurs,
|
||||
piece.constructeur,
|
||||
)
|
||||
const hasResolvedConstructeurs =
|
||||
Array.isArray(piece.constructeurs)
|
||||
&& piece.constructeurs.length > 0
|
||||
&& piece.constructeurs.every((item) => item && typeof item === 'object')
|
||||
Array.isArray(piece.constructeurs) &&
|
||||
piece.constructeurs.length > 0 &&
|
||||
piece.constructeurs.every((item) => item && typeof item === 'object')
|
||||
|
||||
if (ids.length && !hasResolvedConstructeurs) {
|
||||
const resolved = await ensureConstructeurs(ids)
|
||||
@@ -85,16 +123,7 @@ export function usePieces () {
|
||||
return piece
|
||||
}
|
||||
|
||||
/**
|
||||
* Load pieces with pagination and search support
|
||||
* @param {Object} options - Query options
|
||||
* @param {string} [options.search] - Search term for name/reference
|
||||
* @param {number} [options.page=1] - Current page (1-based)
|
||||
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||
* @param {string} [options.orderBy='name'] - Field to order by
|
||||
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||
*/
|
||||
const loadPieces = async (options = {}) => {
|
||||
const loadPieces = async (options: LoadPiecesOptions = {}): Promise<PieceListResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const {
|
||||
@@ -102,7 +131,7 @@ export function usePieces () {
|
||||
page = 1,
|
||||
itemsPerPage = 30,
|
||||
orderBy = 'name',
|
||||
orderDir = 'asc'
|
||||
orderDir = 'asc',
|
||||
} = options
|
||||
|
||||
const params = new URLSearchParams()
|
||||
@@ -110,11 +139,9 @@ export function usePieces () {
|
||||
params.set('page', String(page))
|
||||
|
||||
if (search && search.trim()) {
|
||||
// API Platform uses property filters
|
||||
params.set('name', search.trim())
|
||||
}
|
||||
|
||||
// API Platform OrderFilter syntax: order[field]=direction
|
||||
params.set(`order[${orderBy}]`, orderDir)
|
||||
|
||||
const result = await get(`/pieces?${params.toString()}`)
|
||||
@@ -129,79 +156,84 @@ export function usePieces () {
|
||||
items: enrichedItems,
|
||||
total: total.value,
|
||||
page,
|
||||
itemsPerPage
|
||||
}
|
||||
itemsPerPage,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result as PieceListResult
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des pièces:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createPiece = async (pieceData) => {
|
||||
const createPiece = async (pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
||||
const result = await post('/pieces', normalizedPayload)
|
||||
if (result.success) {
|
||||
const enriched = await withResolvedConstructeurs(result.data)
|
||||
if (result.success && result.data) {
|
||||
const enriched = await withResolvedConstructeurs(result.data as Piece)
|
||||
pieces.value.unshift(enriched)
|
||||
total.value += 1
|
||||
const displayName = result.data?.name
|
||||
|| pieceData?.definition?.name
|
||||
|| pieceData?.name
|
||||
|| 'Pièce'
|
||||
const definition = (pieceData as Record<string, unknown>)?.definition as Record<string, unknown> | undefined
|
||||
const displayName =
|
||||
(result.data as Piece)?.name ||
|
||||
(definition?.name as string | undefined) ||
|
||||
pieceData?.name ||
|
||||
'Pièce'
|
||||
showSuccess(`Pièce "${displayName}" créée avec succès`)
|
||||
return { success: true, data: enriched }
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création de la pièce:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updatePieceData = async (id, pieceData) => {
|
||||
const updatePieceData = async (id: string, pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
||||
const result = await patch(`/pieces/${id}`, normalizedPayload)
|
||||
if (result.success) {
|
||||
const updated = await withResolvedConstructeurs(result.data)
|
||||
const index = pieces.value.findIndex(piece => piece.id === id)
|
||||
if (result.success && result.data) {
|
||||
const updated = await withResolvedConstructeurs(result.data as Piece)
|
||||
const index = pieces.value.findIndex((piece) => piece.id === id)
|
||||
if (index !== -1) {
|
||||
pieces.value[index] = updated
|
||||
}
|
||||
showSuccess(`Pièce "${updated?.name || pieceData.name || ''}" mise à jour avec succès`)
|
||||
return { success: true, data: updated }
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour de la pièce:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deletePiece = async (id) => {
|
||||
const deletePiece = async (id: string): Promise<PieceSingleResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await del(`/pieces/${id}`)
|
||||
if (result.success) {
|
||||
const deletedPiece = pieces.value.find(piece => piece.id === id)
|
||||
pieces.value = pieces.value.filter(piece => piece.id !== id)
|
||||
const deletedPiece = pieces.value.find((piece) => piece.id === id)
|
||||
pieces.value = pieces.value.filter((piece) => piece.id !== id)
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
||||
return { success: true }
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression de la pièce:', error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -219,6 +251,6 @@ export function usePieces () {
|
||||
updatePiece: updatePieceData,
|
||||
deletePiece,
|
||||
getPieces,
|
||||
isLoading
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,34 @@
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { listModelTypes, createModelType, updateModelType, deleteModelType } from '~/services/modelTypes'
|
||||
import { listModelTypes, createModelType, updateModelType, deleteModelType, type ModelType } from '~/services/modelTypes'
|
||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||
|
||||
const productTypes = ref([])
|
||||
export interface ProductType extends ModelType {
|
||||
structure: ProductModelStructure | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
interface ProductTypePayload {
|
||||
name: string
|
||||
code?: string
|
||||
description?: string | null
|
||||
notes?: string | null
|
||||
structure?: ProductModelStructure | null
|
||||
}
|
||||
|
||||
interface ProductTypeResult {
|
||||
success: boolean
|
||||
data?: ProductType | ProductType[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
const productTypes = ref<ProductType[]>([])
|
||||
const loadingProductTypes = ref(false)
|
||||
|
||||
export function useProductTypes () {
|
||||
export function useProductTypes() {
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const generateCodeFromName = (name) => {
|
||||
const generateCodeFromName = (name: string): string => {
|
||||
return (name || '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036F]/g, '')
|
||||
@@ -18,7 +38,7 @@ export function useProductTypes () {
|
||||
.replace(/-+/g, '-') || 'type'
|
||||
}
|
||||
|
||||
const loadProductTypes = async () => {
|
||||
const loadProductTypes = async (): Promise<ProductTypeResult> => {
|
||||
loadingProductTypes.value = true
|
||||
try {
|
||||
const data = await listModelTypes({
|
||||
@@ -28,14 +48,16 @@ export function useProductTypes () {
|
||||
limit: 200,
|
||||
})
|
||||
|
||||
productTypes.value = data.items.map(item => ({
|
||||
productTypes.value = data.items.map((item) => ({
|
||||
...item,
|
||||
structure: item.structure as ProductModelStructure | null,
|
||||
description: item.description ?? item.notes ?? null,
|
||||
}))
|
||||
|
||||
return { success: true, data: productTypes.value }
|
||||
} catch (error) {
|
||||
const message = error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { message?: string }
|
||||
const message = err?.message || 'Erreur inconnue'
|
||||
showError(`Impossible de charger les types de produit: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -43,20 +65,21 @@ export function useProductTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const createProductType = async (payload) => {
|
||||
const createProductType = async (payload: ProductTypePayload): Promise<ProductTypeResult> => {
|
||||
loadingProductTypes.value = true
|
||||
try {
|
||||
const data = await createModelType({
|
||||
name: payload.name,
|
||||
code: payload.code || generateCodeFromName(payload.name),
|
||||
category: 'PRODUCT',
|
||||
notes: payload.description ?? payload.notes,
|
||||
description: payload.description ?? null,
|
||||
structure: payload.structure,
|
||||
notes: payload.description ?? payload.notes ?? undefined,
|
||||
description: payload.description ?? undefined,
|
||||
structure: payload.structure ?? undefined,
|
||||
})
|
||||
|
||||
const normalized = {
|
||||
const normalized: ProductType = {
|
||||
...data,
|
||||
structure: data.structure as ProductModelStructure | null,
|
||||
description: data.description ?? data.notes ?? null,
|
||||
}
|
||||
|
||||
@@ -65,7 +88,8 @@ export function useProductTypes () {
|
||||
|
||||
return { success: true, data: normalized }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la création du type de produit: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -73,23 +97,24 @@ export function useProductTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const updateProductType = async (id, payload) => {
|
||||
const updateProductType = async (id: string, payload: ProductTypePayload): Promise<ProductTypeResult> => {
|
||||
loadingProductTypes.value = true
|
||||
try {
|
||||
const data = await updateModelType(id, {
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
notes: payload.notes,
|
||||
description: payload.description ?? undefined,
|
||||
notes: payload.notes ?? undefined,
|
||||
code: payload.code,
|
||||
structure: payload.structure,
|
||||
structure: payload.structure ?? undefined,
|
||||
})
|
||||
|
||||
const normalized = {
|
||||
const normalized: ProductType = {
|
||||
...data,
|
||||
structure: data.structure as ProductModelStructure | null,
|
||||
description: data.description ?? data.notes ?? null,
|
||||
}
|
||||
|
||||
const index = productTypes.value.findIndex(type => type.id === id)
|
||||
const index = productTypes.value.findIndex((type) => type.id === id)
|
||||
if (index !== -1) {
|
||||
productTypes.value[index] = normalized
|
||||
}
|
||||
@@ -97,7 +122,8 @@ export function useProductTypes () {
|
||||
|
||||
return { success: true, data: normalized }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la mise à jour du type de produit: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -105,15 +131,16 @@ export function useProductTypes () {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteProductType = async (id) => {
|
||||
const deleteProductType = async (id: string): Promise<ProductTypeResult> => {
|
||||
loadingProductTypes.value = true
|
||||
try {
|
||||
await deleteModelType(id)
|
||||
productTypes.value = productTypes.value.filter(type => type.id !== id)
|
||||
productTypes.value = productTypes.value.filter((type) => type.id !== id)
|
||||
showSuccess('Type de produit supprimé')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error?.data?.message || error?.message || 'Erreur inconnue'
|
||||
const err = error as Error & { data?: { message?: string }; message?: string }
|
||||
const message = err?.data?.message || err?.message || 'Erreur inconnue'
|
||||
showError(`Erreur lors de la suppression du type de produit: ${message}`)
|
||||
return { success: false, error: message }
|
||||
} finally {
|
||||
@@ -2,16 +2,52 @@ import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { useApi } from './useApi'
|
||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import { useConstructeurs } from './useConstructeurs'
|
||||
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||
|
||||
const products = ref([])
|
||||
export interface Product {
|
||||
id: string
|
||||
name: string
|
||||
reference?: string | null
|
||||
typeProductId?: string | null
|
||||
typeProduct?: { id: string; name?: string } | null
|
||||
constructeurs?: Constructeur[]
|
||||
constructeurIds?: string[]
|
||||
supplierPrice?: number | null
|
||||
createdAt?: string | null
|
||||
updatedAt?: string | null
|
||||
documents?: unknown[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ProductListResult {
|
||||
success: boolean
|
||||
data?: { items: Product[]; total: number; page: number; itemsPerPage: number }
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ProductSingleResult {
|
||||
success: boolean
|
||||
data?: Product
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface LoadProductsOptions {
|
||||
search?: string
|
||||
page?: number
|
||||
itemsPerPage?: number
|
||||
orderBy?: string
|
||||
orderDir?: 'asc' | 'desc'
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
const products = ref<Product[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
const error = ref(null)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const replaceInCache = (item) => {
|
||||
const replaceInCache = (item: Product): boolean => {
|
||||
if (!item?.id) {
|
||||
return false
|
||||
}
|
||||
@@ -26,38 +62,40 @@ const replaceInCache = (item) => {
|
||||
return false
|
||||
}
|
||||
|
||||
const extractCollection = (payload) => {
|
||||
const extractCollection = (payload: unknown): Product[] => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
return payload as Product[]
|
||||
}
|
||||
if (Array.isArray(payload?.member)) {
|
||||
return payload.member
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (Array.isArray(p?.member)) {
|
||||
return p.member as Product[]
|
||||
}
|
||||
if (Array.isArray(payload?.['hydra:member'])) {
|
||||
return payload['hydra:member']
|
||||
if (Array.isArray(p?.['hydra:member'])) {
|
||||
return p['hydra:member'] as Product[]
|
||||
}
|
||||
if (Array.isArray(payload?.data)) {
|
||||
return payload.data
|
||||
if (Array.isArray(p?.data)) {
|
||||
return p.data as Product[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const extractTotal = (payload, fallbackLength) => {
|
||||
if (typeof payload?.totalItems === 'number') {
|
||||
return payload.totalItems
|
||||
const extractTotal = (payload: unknown, fallbackLength: number): number => {
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (typeof p?.totalItems === 'number') {
|
||||
return p.totalItems
|
||||
}
|
||||
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
||||
return payload['hydra:totalItems']
|
||||
if (typeof p?.['hydra:totalItems'] === 'number') {
|
||||
return p['hydra:totalItems']
|
||||
}
|
||||
return fallbackLength
|
||||
}
|
||||
|
||||
export function useProducts () {
|
||||
export function useProducts() {
|
||||
const { showError } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
const { ensureConstructeurs } = useConstructeurs()
|
||||
|
||||
const withResolvedConstructeurs = async (product) => {
|
||||
const withResolvedConstructeurs = async (product: Product): Promise<Product> => {
|
||||
if (!product || typeof product !== 'object') {
|
||||
return product
|
||||
}
|
||||
@@ -70,12 +108,11 @@ export function useProducts () {
|
||||
const ids = uniqueConstructeurIds(
|
||||
product.constructeurIds,
|
||||
product.constructeurs,
|
||||
product.constructeur,
|
||||
)
|
||||
const hasResolvedConstructeurs =
|
||||
Array.isArray(product.constructeurs)
|
||||
&& product.constructeurs.length > 0
|
||||
&& product.constructeurs.every((item) => item && typeof item === 'object')
|
||||
Array.isArray(product.constructeurs) &&
|
||||
product.constructeurs.length > 0 &&
|
||||
product.constructeurs.every((item) => item && typeof item === 'object')
|
||||
|
||||
if (ids.length && !hasResolvedConstructeurs) {
|
||||
const resolved = await ensureConstructeurs(ids)
|
||||
@@ -87,24 +124,13 @@ export function useProducts () {
|
||||
return product
|
||||
}
|
||||
|
||||
/**
|
||||
* Load products with pagination and search support
|
||||
* @param {Object} options - Query options
|
||||
* @param {string} [options.search] - Search term for name/reference
|
||||
* @param {number} [options.page=1] - Current page (1-based)
|
||||
* @param {number} [options.itemsPerPage=30] - Items per page
|
||||
* @param {string} [options.orderBy='name'] - Field to order by
|
||||
* @param {string} [options.orderDir='asc'] - Order direction (asc/desc)
|
||||
* @param {boolean} [options.force=false] - Force reload even if already loaded
|
||||
*/
|
||||
const loadProducts = async (options = {}) => {
|
||||
const loadProducts = async (options: LoadProductsOptions = {}): Promise<ProductListResult> => {
|
||||
const {
|
||||
search = '',
|
||||
page = 1,
|
||||
itemsPerPage = 30,
|
||||
orderBy = 'name',
|
||||
orderDir = 'asc',
|
||||
force = false
|
||||
} = options
|
||||
|
||||
if (loading.value) {
|
||||
@@ -140,17 +166,17 @@ export function useProducts () {
|
||||
items: enrichedItems,
|
||||
total: total.value,
|
||||
page,
|
||||
itemsPerPage
|
||||
}
|
||||
itemsPerPage,
|
||||
},
|
||||
}
|
||||
} else if (result.error) {
|
||||
error.value = result.error
|
||||
showError(`Impossible de charger les produits: ${result.error}`)
|
||||
}
|
||||
return result
|
||||
return result as ProductListResult
|
||||
} catch (err) {
|
||||
console.error('Erreur lors du chargement des produits:', err)
|
||||
const message = err?.message ?? 'Erreur inconnue'
|
||||
const message = (err as Error)?.message ?? 'Erreur inconnue'
|
||||
error.value = message
|
||||
showError(`Impossible de charger les produits: ${message}`)
|
||||
return { success: false, error: message }
|
||||
@@ -159,26 +185,27 @@ export function useProducts () {
|
||||
}
|
||||
}
|
||||
|
||||
const createProduct = async (payload) => {
|
||||
const createProduct = async (payload: Partial<Product>): Promise<ProductSingleResult> => {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await post('/products', normalizedPayload)
|
||||
if (result.success && result.data) {
|
||||
const enriched = await withResolvedConstructeurs(result.data)
|
||||
const enriched = await withResolvedConstructeurs(result.data as Product)
|
||||
const added = replaceInCache(enriched)
|
||||
if (added) {
|
||||
total.value += 1
|
||||
}
|
||||
return { success: true, data: enriched }
|
||||
} else if (result.error) {
|
||||
error.value = result.error
|
||||
showError(result.error)
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (err) {
|
||||
console.error('Erreur lors de la création du produit:', err)
|
||||
const message = err?.message ?? 'Erreur inconnue'
|
||||
const message = (err as Error)?.message ?? 'Erreur inconnue'
|
||||
error.value = message
|
||||
showError(message)
|
||||
return { success: false, error: message }
|
||||
@@ -187,23 +214,24 @@ export function useProducts () {
|
||||
}
|
||||
}
|
||||
|
||||
const updateProduct = async (id, payload) => {
|
||||
const updateProduct = async (id: string, payload: Partial<Product>): Promise<ProductSingleResult> => {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await patch(`/products/${id}`, normalizedPayload)
|
||||
if (result.success && result.data) {
|
||||
const enriched = await withResolvedConstructeurs(result.data)
|
||||
const enriched = await withResolvedConstructeurs(result.data as Product)
|
||||
replaceInCache(enriched)
|
||||
return { success: true, data: enriched }
|
||||
} else if (result.error) {
|
||||
error.value = result.error
|
||||
showError(result.error)
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (err) {
|
||||
console.error('Erreur lors de la mise à jour du produit:', err)
|
||||
const message = err?.message ?? 'Erreur inconnue'
|
||||
const message = (err as Error)?.message ?? 'Erreur inconnue'
|
||||
error.value = message
|
||||
showError(message)
|
||||
return { success: false, error: message }
|
||||
@@ -212,23 +240,23 @@ export function useProducts () {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteProduct = async (id) => {
|
||||
const deleteProduct = async (id: string): Promise<ProductSingleResult> => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await del(`/products/${id}`)
|
||||
if (result.success) {
|
||||
const removed = products.value.find((product) => product.id === id)
|
||||
products.value = products.value.filter((product) => product.id !== id)
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
return { success: true }
|
||||
} else if (result.error) {
|
||||
error.value = result.error
|
||||
showError(result.error)
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (err) {
|
||||
console.error('Erreur lors de la suppression du produit:', err)
|
||||
const message = err?.message ?? 'Erreur inconnue'
|
||||
const message = (err as Error)?.message ?? 'Erreur inconnue'
|
||||
error.value = message
|
||||
showError(message)
|
||||
return { success: false, error: message }
|
||||
@@ -237,7 +265,7 @@ export function useProducts () {
|
||||
}
|
||||
}
|
||||
|
||||
const getProduct = async (id, options = {}) => {
|
||||
const getProduct = async (id: string, options: { force?: boolean } = {}): Promise<ProductSingleResult> => {
|
||||
const shouldForce = !!options.force
|
||||
if (!shouldForce) {
|
||||
const cached = products.value.find((product) => product.id === id)
|
||||
@@ -249,14 +277,14 @@ export function useProducts () {
|
||||
try {
|
||||
const result = await get(`/products/${id}`)
|
||||
if (result.success && result.data) {
|
||||
const enriched = await withResolvedConstructeurs(result.data)
|
||||
const enriched = await withResolvedConstructeurs(result.data as Product)
|
||||
replaceInCache(enriched)
|
||||
return { success: true, data: enriched }
|
||||
}
|
||||
return result
|
||||
return { success: false, error: result.error }
|
||||
} catch (err) {
|
||||
console.error('Erreur lors du chargement du produit:', err)
|
||||
const message = err?.message ?? 'Erreur inconnue'
|
||||
const message = (err as Error)?.message ?? 'Erreur inconnue'
|
||||
return { success: false, error: message }
|
||||
}
|
||||
}
|
||||
@@ -166,8 +166,9 @@ export function useSiteManagement() {
|
||||
)
|
||||
uploadingDocuments.value = false
|
||||
|
||||
if (uploadResult.success) {
|
||||
uploadedDocuments = uploadResult.data || []
|
||||
if (uploadResult.success && uploadResult.data) {
|
||||
const data = uploadResult.data
|
||||
uploadedDocuments = (Array.isArray(data) ? data : [data]) as SiteDocument[]
|
||||
selectedFiles.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { useApi } from './useApi'
|
||||
|
||||
const sites = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
export function useSites () {
|
||||
const { showSuccess, showInfo } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
|
||||
const loadSites = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await get('/sites')
|
||||
console.log('sites api result', result)
|
||||
|
||||
if (result.success) {
|
||||
const collection = Array.isArray(result.data)
|
||||
? result.data
|
||||
: Array.isArray(result.data?.member)
|
||||
? result.data.member
|
||||
: Array.isArray(result.data?.['hydra:member'])
|
||||
? result.data['hydra:member']
|
||||
: Array.isArray(result.data?.data)
|
||||
? result.data.data
|
||||
: []
|
||||
sites.value = collection
|
||||
showInfo(`Chargement de ${collection.length} site(s) réussi`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des sites:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createSite = async (siteData) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await post('/sites', siteData)
|
||||
if (result.success) {
|
||||
sites.value.push(result.data)
|
||||
showSuccess(`Site "${siteData.name}" créé avec succès`)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du site:', error)
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateSite = async (id, siteData) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await patch(`/sites/${id}`, siteData)
|
||||
if (result.success) {
|
||||
const index = sites.value.findIndex(site => site.id === id)
|
||||
if (index !== -1) {
|
||||
sites.value[index] = result.data
|
||||
}
|
||||
showSuccess(`Site "${siteData.name}" mis à jour avec succès`)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour du site:', error)
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSite = async (id) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await del(`/sites/${id}`)
|
||||
if (result.success) {
|
||||
const deletedSite = sites.value.find(site => site.id === id)
|
||||
sites.value = sites.value.filter(site => site.id !== id)
|
||||
showSuccess(`Site "${deletedSite?.name || 'inconnu'}" supprimé avec succès`)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression du site:', error)
|
||||
return { success: false, error: error.message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getSiteById = (id) => {
|
||||
return sites.value.find(site => site.id === id)
|
||||
}
|
||||
|
||||
const getSites = () => sites.value
|
||||
const isLoading = () => loading.value
|
||||
|
||||
return {
|
||||
sites,
|
||||
loading,
|
||||
loadSites,
|
||||
createSite,
|
||||
updateSite,
|
||||
deleteSite,
|
||||
getSiteById,
|
||||
getSites,
|
||||
isLoading
|
||||
}
|
||||
}
|
||||
139
app/composables/useSites.ts
Normal file
139
app/composables/useSites.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { useApi } from './useApi'
|
||||
|
||||
export interface Site {
|
||||
id: string
|
||||
name?: string
|
||||
contactName?: string
|
||||
contactPhone?: string
|
||||
contactAddress?: string
|
||||
contactPostalCode?: string
|
||||
contactCity?: string
|
||||
machines?: unknown[]
|
||||
documents?: unknown[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface SiteResult {
|
||||
success: boolean
|
||||
data?: Site
|
||||
error?: string
|
||||
}
|
||||
|
||||
const sites = ref<Site[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const extractCollection = (payload: unknown): Site[] => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload as Site[]
|
||||
}
|
||||
const p = payload as Record<string, unknown> | null
|
||||
if (Array.isArray(p?.member)) {
|
||||
return p.member as Site[]
|
||||
}
|
||||
if (Array.isArray(p?.['hydra:member'])) {
|
||||
return p['hydra:member'] as Site[]
|
||||
}
|
||||
if (Array.isArray(p?.data)) {
|
||||
return p.data as Site[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function useSites() {
|
||||
const { showSuccess, showInfo } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
|
||||
const loadSites = async (): Promise<void> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await get('/sites')
|
||||
console.log('sites api result', result)
|
||||
|
||||
if (result.success) {
|
||||
const collection = extractCollection(result.data)
|
||||
sites.value = collection
|
||||
showInfo(`Chargement de ${collection.length} site(s) réussi`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des sites:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createSite = async (siteData: Partial<Site>): Promise<SiteResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await post('/sites', siteData)
|
||||
if (result.success && result.data) {
|
||||
sites.value.push(result.data as Site)
|
||||
showSuccess(`Site "${siteData.name}" créé avec succès`)
|
||||
}
|
||||
return result as SiteResult
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du site:', error)
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateSite = async (id: string, siteData: Partial<Site>): Promise<SiteResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await patch(`/sites/${id}`, siteData)
|
||||
if (result.success && result.data) {
|
||||
const index = sites.value.findIndex((site) => site.id === id)
|
||||
if (index !== -1) {
|
||||
sites.value[index] = result.data as Site
|
||||
}
|
||||
showSuccess(`Site "${siteData.name}" mis à jour avec succès`)
|
||||
}
|
||||
return result as SiteResult
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour du site:', error)
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSite = async (id: string): Promise<SiteResult> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await del(`/sites/${id}`)
|
||||
if (result.success) {
|
||||
const deletedSite = sites.value.find((site) => site.id === id)
|
||||
sites.value = sites.value.filter((site) => site.id !== id)
|
||||
showSuccess(`Site "${deletedSite?.name || 'inconnu'}" supprimé avec succès`)
|
||||
}
|
||||
return result as SiteResult
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression du site:', error)
|
||||
return { success: false, error: (error as Error).message }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getSiteById = (id: string): Site | undefined => {
|
||||
return sites.value.find((site) => site.id === id)
|
||||
}
|
||||
|
||||
const getSites = () => sites.value
|
||||
const isLoading = () => loading.value
|
||||
|
||||
return {
|
||||
sites,
|
||||
loading,
|
||||
loadSites,
|
||||
createSite,
|
||||
updateSite,
|
||||
deleteSite,
|
||||
getSiteById,
|
||||
getSites,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
@@ -539,26 +539,34 @@ import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/mo
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||
import type { ModelType } from '~/services/modelTypes'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
fieldKey,
|
||||
buildCustomFieldInputs,
|
||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||
saveCustomFieldValues as _saveCustomFieldValues,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
import {
|
||||
documentIcon,
|
||||
formatSize,
|
||||
shouldInlinePdf,
|
||||
documentPreviewSrc,
|
||||
documentThumbnailClass,
|
||||
downloadDocument,
|
||||
} from '~/shared/utils/documentDisplayUtils'
|
||||
import {
|
||||
historyActionLabel,
|
||||
formatHistoryDate,
|
||||
formatHistoryValue,
|
||||
historyDiffEntries as _historyDiffEntries,
|
||||
} from '~/shared/utils/historyDisplayUtils'
|
||||
|
||||
interface ComponentCatalogType extends ModelType {
|
||||
structure: ComponentModelStructure | null
|
||||
customFields?: Array<Record<string, any>>
|
||||
}
|
||||
|
||||
interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
customFieldValueId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { get } = useApi()
|
||||
@@ -601,75 +609,8 @@ const historyFieldLabels: Record<string, string> = {
|
||||
constructeurIds: 'Fournisseurs',
|
||||
}
|
||||
|
||||
const historyActionLabel = (action: string) => {
|
||||
if (action === 'create') {
|
||||
return 'Création'
|
||||
}
|
||||
if (action === 'delete') {
|
||||
return 'Suppression'
|
||||
}
|
||||
return 'Modification'
|
||||
}
|
||||
|
||||
const historyDateFormatter = new Intl.DateTimeFormat('fr-FR', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
const formatHistoryDate = (value: string) => {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
return historyDateFormatter.format(date)
|
||||
}
|
||||
|
||||
const formatHistoryValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '—'
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return '—'
|
||||
}
|
||||
return value.map((item) => formatHistoryValue(item)).join(', ')
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const maybeRecord = value as Record<string, unknown>
|
||||
const name = typeof maybeRecord.name === 'string' ? maybeRecord.name : null
|
||||
const id = typeof maybeRecord.id === 'string' ? maybeRecord.id : null
|
||||
if (name && id) {
|
||||
return `${name} (#${id})`
|
||||
}
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
if (id) {
|
||||
return `#${id}`
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch (error) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const historyDiffEntries = (entry: ComponentHistoryEntry) => {
|
||||
const diff = entry.diff ?? {}
|
||||
return Object.entries(diff).map(([field, change]) => {
|
||||
const label = historyFieldLabels[field] ?? field
|
||||
const fromLabel = formatHistoryValue(change?.from)
|
||||
const toLabel = formatHistoryValue(change?.to)
|
||||
return {
|
||||
field,
|
||||
label,
|
||||
fromLabel,
|
||||
toLabel,
|
||||
}
|
||||
})
|
||||
}
|
||||
const historyDiffEntries = (entry: ComponentHistoryEntry) =>
|
||||
_historyDiffEntries(entry, historyFieldLabels)
|
||||
const selectedTypeId = ref<string>('')
|
||||
const editionForm = reactive({
|
||||
name: '' as string,
|
||||
@@ -679,49 +620,6 @@ const editionForm = reactive({
|
||||
})
|
||||
|
||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||
const documentIcon = (doc: any) =>
|
||||
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
||||
const formatSize = (size: number | null | undefined) => {
|
||||
if (size === null || size === undefined) {
|
||||
return '—'
|
||||
}
|
||||
if (size === 0) {
|
||||
return '0 B'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
||||
const formatted = size / Math.pow(1024, index)
|
||||
return `${formatted.toFixed(1)} ${units[index]}`
|
||||
}
|
||||
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||
const shouldInlinePdf = (document: any) => {
|
||||
if (!document || !isPdfDocument(document) || !document.path) {
|
||||
return false
|
||||
}
|
||||
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const appendPdfViewerParams = (src: string) => {
|
||||
if (!src || src.startsWith('data:')) {
|
||||
return src || ''
|
||||
}
|
||||
if (src.includes('#')) {
|
||||
return `${src}&toolbar=0&navpanes=0`
|
||||
}
|
||||
return `${src}#toolbar=0&navpanes=0`
|
||||
}
|
||||
const documentPreviewSrc = (document: any) => {
|
||||
if (!document?.path) {
|
||||
return ''
|
||||
}
|
||||
if (isPdfDocument(document)) {
|
||||
return appendPdfViewerParams(document.path)
|
||||
}
|
||||
return document.path
|
||||
}
|
||||
|
||||
const fetchedPieceTypeMap = ref<Record<string, string>>({})
|
||||
const pieceTypeLabelMap = computed(() => ({
|
||||
...Object.fromEntries(
|
||||
@@ -761,12 +659,6 @@ const componentCatalogMap = computed(() =>
|
||||
.map((item: any) => [String(item.id), item]),
|
||||
),
|
||||
)
|
||||
const documentThumbnailClass = (document: any) => {
|
||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||
return 'h-24 w-20'
|
||||
}
|
||||
return 'h-16 w-16'
|
||||
}
|
||||
const openPreview = (doc: any) => {
|
||||
if (!doc || !canPreviewDocument(doc)) {
|
||||
return
|
||||
@@ -778,20 +670,6 @@ const closePreview = () => {
|
||||
previewVisible.value = false
|
||||
previewDocument.value = null
|
||||
}
|
||||
const downloadDocument = (doc: any) => {
|
||||
if (!doc?.path) {
|
||||
return
|
||||
}
|
||||
const target = String(doc.path)
|
||||
if (target.startsWith('data:')) {
|
||||
const link = document.createElement('a')
|
||||
link.href = target
|
||||
link.download = doc.filename || doc.name || 'document'
|
||||
link.click()
|
||||
return
|
||||
}
|
||||
window.open(target, '_blank')
|
||||
}
|
||||
const removeDocument = async (documentId: string | number | null | undefined) => {
|
||||
if (!documentId) {
|
||||
return
|
||||
@@ -865,15 +743,7 @@ const refreshCustomFieldInputs = (
|
||||
}
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
customFieldInputs.value.every((field) => {
|
||||
if (!field.required) {
|
||||
return true
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}),
|
||||
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||
)
|
||||
|
||||
const canSubmit = computed(() => Boolean(
|
||||
@@ -883,19 +753,6 @@ const canSubmit = computed(() => Boolean(
|
||||
!saving.value,
|
||||
))
|
||||
|
||||
const toFieldString = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const fetchComponent = async () => {
|
||||
const id = route.params.id
|
||||
if (!id || typeof id !== 'string') {
|
||||
@@ -996,7 +853,15 @@ const submitEdition = async () => {
|
||||
const result = await updateComposant(component.value.id, payload)
|
||||
if (result.success) {
|
||||
const updatedComponent = result.data
|
||||
await saveCustomFieldValues(updatedComponent)
|
||||
await _saveCustomFieldValues(
|
||||
'composant',
|
||||
updatedComponent.id,
|
||||
[
|
||||
updatedComponent?.typeComposant?.customFields,
|
||||
updatedComponent?.typeMachineComponentRequirement?.typeComposant?.customFields,
|
||||
],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
await router.push('/component-catalog')
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -1006,260 +871,6 @@ const submitEdition = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildCustomFieldInputs = (
|
||||
structure: ComponentModelStructure | null,
|
||||
values: any[] | null,
|
||||
): CustomFieldInput[] => {
|
||||
const normalizedStructure = structure ? normalizeStructureForEditor(structure) : null
|
||||
const definitions = normalizeCustomFieldInputs(normalizedStructure)
|
||||
const valueList = Array.isArray(values) ? values : []
|
||||
|
||||
const mapById = new Map<string, any>()
|
||||
const mapByName = new Map<string, any>()
|
||||
|
||||
valueList.forEach((entry) => {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return
|
||||
}
|
||||
const fieldId = entry.customField?.id || entry.customFieldId || null
|
||||
if (fieldId) {
|
||||
mapById.set(fieldId, entry)
|
||||
}
|
||||
const fieldName = entry.customField?.name || entry.name || entry.key || null
|
||||
if (fieldName) {
|
||||
mapByName.set(fieldName, entry)
|
||||
}
|
||||
})
|
||||
|
||||
const resolved: CustomFieldInput[] = definitions.map((definition) => {
|
||||
const definitionId = definition.customFieldId || definition.id || null
|
||||
const matched = (definitionId ? mapById.get(definitionId) : null) || mapByName.get(definition.name)
|
||||
|
||||
if (!matched) {
|
||||
return {
|
||||
...definition,
|
||||
customFieldId: definition.customFieldId || definition.id,
|
||||
customFieldValueId: null,
|
||||
orderIndex: definition.orderIndex,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedValue = extractStoredCustomFieldValue(matched)
|
||||
return {
|
||||
...definition,
|
||||
customFieldId: matched.customField?.id || definition.customFieldId || definition.id,
|
||||
customFieldValueId: matched.id ?? null,
|
||||
value: formatDefaultValue(definition.type, resolvedValue),
|
||||
orderIndex: Math.min(
|
||||
definition.orderIndex ?? 0,
|
||||
typeof matched.customField?.orderIndex === 'number'
|
||||
? matched.customField.orderIndex
|
||||
: definition.orderIndex ?? 0,
|
||||
),
|
||||
}
|
||||
}).sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldValueId || field.id || `${field.name}-${index}`
|
||||
|
||||
const normalizeCustomFieldInputs = (structure: ComponentModelStructure | null): CustomFieldInput[] => {
|
||||
if (!structure || typeof structure !== 'object') {
|
||||
return []
|
||||
}
|
||||
const fields = Array.isArray(structure.customFields) ? structure.customFields : []
|
||||
return fields
|
||||
.map((field, index) => normalizeCustomField(field, index))
|
||||
.filter((field): field is CustomFieldInput => field !== null)
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
|
||||
if (!rawField || typeof rawField !== 'object') {
|
||||
return null
|
||||
}
|
||||
const name = resolveFieldName(rawField)
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
const type = resolveFieldType(rawField)
|
||||
const required = resolveRequiredFlag(rawField)
|
||||
const options = resolveOptions(rawField)
|
||||
const defaultSource = resolveDefaultValue(rawField)
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
|
||||
? rawField.customFieldValueId
|
||||
: null
|
||||
const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
|
||||
return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex }
|
||||
}
|
||||
|
||||
const resolveFieldName = (field: any): string => {
|
||||
if (typeof field?.name === 'string' && field.name.trim()) {
|
||||
return field.name.trim()
|
||||
}
|
||||
if (typeof field?.key === 'string' && field.key.trim()) {
|
||||
return field.key.trim()
|
||||
}
|
||||
if (typeof field?.label === 'string' && field.label.trim()) {
|
||||
return field.label.trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveFieldType = (field: any): string => {
|
||||
const allowed = ['text', 'number', 'select', 'boolean', 'date']
|
||||
const rawType =
|
||||
typeof field?.type === 'string'
|
||||
? field.type
|
||||
: typeof field?.value?.type === 'string'
|
||||
? field.value.type
|
||||
: ''
|
||||
const value = rawType.toLowerCase()
|
||||
return allowed.includes(value) ? value : 'text'
|
||||
}
|
||||
|
||||
const resolveDefaultValue = (field: any): any => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return null
|
||||
}
|
||||
if (field.defaultValue !== undefined && field.defaultValue !== null) {
|
||||
return field.defaultValue
|
||||
}
|
||||
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
|
||||
return field.value
|
||||
}
|
||||
if (field.default !== undefined && field.default !== null) {
|
||||
return field.default
|
||||
}
|
||||
if (field.value && typeof field.value === 'object') {
|
||||
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
|
||||
return (field.value as any).defaultValue
|
||||
}
|
||||
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
|
||||
return (field.value as any).value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof defaultValue === 'object') {
|
||||
if (defaultValue === null) {
|
||||
return ''
|
||||
}
|
||||
if ('defaultValue' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
|
||||
}
|
||||
if ('value' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') {
|
||||
return 'true'
|
||||
}
|
||||
if (normalized === 'false' || normalized === '0') {
|
||||
return 'false'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return String(defaultValue)
|
||||
}
|
||||
|
||||
const resolveRequiredFlag = (field: any): boolean => {
|
||||
if (typeof field?.required === 'boolean') {
|
||||
return field.required
|
||||
}
|
||||
const nestedRequired = field?.value?.required
|
||||
if (typeof nestedRequired === 'boolean') {
|
||||
return nestedRequired
|
||||
}
|
||||
if (typeof nestedRequired === 'string') {
|
||||
const normalized = nestedRequired.toLowerCase()
|
||||
return normalized === 'true' || normalized === '1'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const resolveOptions = (field: any): string[] => {
|
||||
const sources = [field?.options, field?.value?.options, field?.value?.choices]
|
||||
for (const source of sources) {
|
||||
if (Array.isArray(source)) {
|
||||
const mapped = source
|
||||
.map((option: unknown) => {
|
||||
if (option === null || option === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof option === 'string') {
|
||||
return option.trim()
|
||||
}
|
||||
if (typeof option === 'object') {
|
||||
const record = option || {}
|
||||
const keys = ['value', 'label', 'name']
|
||||
for (const key of keys) {
|
||||
const candidate = record[key]
|
||||
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
||||
return candidate.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
const fallback = String(option).trim()
|
||||
return fallback === '[object Object]' ? '' : fallback
|
||||
})
|
||||
.filter((option) => option.length > 0)
|
||||
if (mapped.length) {
|
||||
return mapped
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const extractStoredCustomFieldValue = (entry: any): any => {
|
||||
if (entry === null || entry === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean') {
|
||||
return entry
|
||||
}
|
||||
if (typeof entry !== 'object') {
|
||||
return String(entry)
|
||||
}
|
||||
const direct = entry.value
|
||||
if (direct !== undefined && direct !== null) {
|
||||
if (typeof direct === 'object') {
|
||||
if (direct === null) {
|
||||
return ''
|
||||
}
|
||||
if ('value' in direct && direct.value !== undefined && direct.value !== null) {
|
||||
return direct.value
|
||||
}
|
||||
if ('defaultValue' in direct && direct.defaultValue !== undefined && direct.defaultValue !== null) {
|
||||
return direct.defaultValue
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return direct
|
||||
}
|
||||
if (entry.defaultValue !== undefined && entry.defaultValue !== null) {
|
||||
return entry.defaultValue
|
||||
}
|
||||
if (entry.customFieldValue?.value !== undefined && entry.customFieldValue?.value !== null) {
|
||||
return entry.customFieldValue.value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
|
||||
return Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||
}
|
||||
@@ -1513,104 +1124,6 @@ const structureSelections = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||
customFieldName: field.name,
|
||||
customFieldType: field.type,
|
||||
customFieldRequired: field.required,
|
||||
customFieldOptions: field.options,
|
||||
})
|
||||
|
||||
const shouldPersistField = (field: CustomFieldInput) => {
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}
|
||||
|
||||
const formatValueForPersistence = (field: CustomFieldInput) => {
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' ? 'true' : 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim()
|
||||
}
|
||||
|
||||
const saveCustomFieldValues = async (updatedComponent: any) => {
|
||||
if (!updatedComponent || !updatedComponent.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const definitionMap = new Map<string, string>()
|
||||
const registerDefinitions = (fields: any[]) => {
|
||||
if (!Array.isArray(fields)) {
|
||||
return
|
||||
}
|
||||
fields.forEach((field) => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return
|
||||
}
|
||||
const name = typeof field.name === 'string' ? field.name : null
|
||||
const id = typeof field.id === 'string' ? field.id : null
|
||||
if (name && id && !definitionMap.has(name)) {
|
||||
definitionMap.set(name, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerDefinitions(updatedComponent?.typeComposant?.customFields)
|
||||
registerDefinitions(updatedComponent?.typeMachineComponentRequirement?.typeComposant?.customFields)
|
||||
|
||||
const resolveDefinitionId = (field: CustomFieldInput) => {
|
||||
if (field.customFieldId) {
|
||||
return field.customFieldId
|
||||
}
|
||||
if (field.id) {
|
||||
return field.id
|
||||
}
|
||||
return definitionMap.get(field.name) ?? null
|
||||
}
|
||||
|
||||
for (const field of customFieldInputs.value) {
|
||||
if (!shouldPersistField(field)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const definitionId = resolveDefinitionId(field)
|
||||
const metadata = definitionId ? undefined : buildCustomFieldMetadata(field)
|
||||
const value = formatValueForPersistence(field)
|
||||
|
||||
if (field.customFieldValueId) {
|
||||
const result = await updateCustomFieldValue(field.customFieldValueId, { value })
|
||||
if (!result.success) {
|
||||
toast.showError(`Impossible de mettre à jour le champ personnalisé "${field.name}"`)
|
||||
} else if (definitionId && !field.customFieldId) {
|
||||
field.customFieldId = definitionId
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await upsertCustomFieldValue(
|
||||
definitionId,
|
||||
'composant',
|
||||
updatedComponent.id,
|
||||
value,
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
toast.showError(`Impossible d'enregistrer le champ personnalisé "${field.name}"`)
|
||||
} else {
|
||||
const createdValue = result.data
|
||||
if (createdValue?.id) {
|
||||
field.customFieldValueId = createdValue.id
|
||||
}
|
||||
const resolvedId = createdValue?.customField?.id || definitionId
|
||||
if (resolvedId) {
|
||||
field.customFieldId = resolvedId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.allSettled([
|
||||
loadComponentTypes(),
|
||||
|
||||
@@ -360,6 +360,10 @@ import { useToast } from '~/composables/useToast'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||
import {
|
||||
toFieldString,
|
||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import type {
|
||||
ComponentModelPiece,
|
||||
@@ -747,15 +751,7 @@ const serializeStructureAssignments = (
|
||||
}
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
customFieldInputs.value.every((field) => {
|
||||
if (!field.required) {
|
||||
return true
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}),
|
||||
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||
)
|
||||
|
||||
const canSubmit = computed(() => Boolean(
|
||||
@@ -766,19 +762,6 @@ const canSubmit = computed(() => Boolean(
|
||||
!submitting.value,
|
||||
))
|
||||
|
||||
const toFieldString = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
|
||||
return Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||
}
|
||||
|
||||
@@ -479,31 +479,39 @@ import { useDocuments } from '~/composables/useDocuments'
|
||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||
import { usePieceHistory, type PieceHistoryEntry } from '~/composables/usePieceHistory'
|
||||
import { extractRelationId } from '~/shared/apiRelations'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
||||
import type { ModelType } from '~/services/modelTypes'
|
||||
import { getModelType } from '~/services/modelTypes'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
fieldKey,
|
||||
buildCustomFieldInputs,
|
||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||
saveCustomFieldValues as _saveCustomFieldValues,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
import {
|
||||
documentIcon,
|
||||
formatSize,
|
||||
shouldInlinePdf,
|
||||
documentPreviewSrc,
|
||||
documentThumbnailClass,
|
||||
downloadDocument,
|
||||
} from '~/shared/utils/documentDisplayUtils'
|
||||
import {
|
||||
historyActionLabel,
|
||||
formatHistoryDate,
|
||||
formatHistoryValue,
|
||||
historyDiffEntries as _historyDiffEntries,
|
||||
} from '~/shared/utils/historyDisplayUtils'
|
||||
|
||||
interface PieceCatalogType extends ModelType {
|
||||
structure: PieceModelStructure | null
|
||||
customFields?: Array<Record<string, any>>
|
||||
}
|
||||
|
||||
interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
customFieldValueId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { get } = useApi()
|
||||
@@ -542,75 +550,8 @@ const historyFieldLabels: Record<string, string> = {
|
||||
constructeurIds: 'Fournisseurs',
|
||||
}
|
||||
|
||||
const historyActionLabel = (action: string) => {
|
||||
if (action === 'create') {
|
||||
return 'Création'
|
||||
}
|
||||
if (action === 'delete') {
|
||||
return 'Suppression'
|
||||
}
|
||||
return 'Modification'
|
||||
}
|
||||
|
||||
const historyDateFormatter = new Intl.DateTimeFormat('fr-FR', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
const formatHistoryDate = (value: string) => {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
return historyDateFormatter.format(date)
|
||||
}
|
||||
|
||||
const formatHistoryValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '—'
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return '—'
|
||||
}
|
||||
return value.map((item) => formatHistoryValue(item)).join(', ')
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const maybeRecord = value as Record<string, unknown>
|
||||
const name = typeof maybeRecord.name === 'string' ? maybeRecord.name : null
|
||||
const id = typeof maybeRecord.id === 'string' ? maybeRecord.id : null
|
||||
if (name && id) {
|
||||
return `${name} (#${id})`
|
||||
}
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
if (id) {
|
||||
return `#${id}`
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch (error) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const historyDiffEntries = (entry: PieceHistoryEntry) => {
|
||||
const diff = entry.diff ?? {}
|
||||
return Object.entries(diff).map(([field, change]) => {
|
||||
const label = historyFieldLabels[field] ?? field
|
||||
const fromLabel = formatHistoryValue(change?.from)
|
||||
const toLabel = formatHistoryValue(change?.to)
|
||||
return {
|
||||
field,
|
||||
label,
|
||||
fromLabel,
|
||||
toLabel,
|
||||
}
|
||||
})
|
||||
}
|
||||
const historyDiffEntries = (entry: PieceHistoryEntry) =>
|
||||
_historyDiffEntries(entry, historyFieldLabels)
|
||||
|
||||
const selectedTypeId = ref<string>('')
|
||||
const pieceTypeDetails = ref<any | null>(null)
|
||||
@@ -623,8 +564,6 @@ const editionForm = reactive({
|
||||
const productSelections = ref<(string | null)[]>([])
|
||||
|
||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||
const documentIcon = (doc: any) =>
|
||||
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
||||
const resolvedStructure = computed<PieceModelStructure | null>(() =>
|
||||
pieceTypeDetails.value?.structure ?? selectedType.value?.structure ?? null,
|
||||
)
|
||||
@@ -637,52 +576,6 @@ const refreshCustomFieldInputs = (
|
||||
const values = valuesOverride ?? piece.value?.customFieldValues ?? null
|
||||
customFieldInputs.value = buildCustomFieldInputs(structure, values)
|
||||
}
|
||||
const formatSize = (size: number | null | undefined) => {
|
||||
if (size === null || size === undefined) {
|
||||
return '—'
|
||||
}
|
||||
if (size === 0) {
|
||||
return '0 B'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
||||
const formatted = size / Math.pow(1024, index)
|
||||
return `${formatted.toFixed(1)} ${units[index]}`
|
||||
}
|
||||
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||
const shouldInlinePdf = (document: any) => {
|
||||
if (!document || !isPdfDocument(document) || !document.path) {
|
||||
return false
|
||||
}
|
||||
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const appendPdfViewerParams = (src: string) => {
|
||||
if (!src || src.startsWith('data:')) {
|
||||
return src || ''
|
||||
}
|
||||
if (src.includes('#')) {
|
||||
return `${src}&toolbar=0&navpanes=0`
|
||||
}
|
||||
return `${src}#toolbar=0&navpanes=0`
|
||||
}
|
||||
const documentPreviewSrc = (document: any) => {
|
||||
if (!document?.path) {
|
||||
return ''
|
||||
}
|
||||
if (isPdfDocument(document)) {
|
||||
return appendPdfViewerParams(document.path)
|
||||
}
|
||||
return document.path
|
||||
}
|
||||
const documentThumbnailClass = (document: any) => {
|
||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||
return 'h-24 w-20'
|
||||
}
|
||||
return 'h-16 w-16'
|
||||
}
|
||||
const openPreview = (doc: any) => {
|
||||
if (!doc || !canPreviewDocument(doc)) {
|
||||
return
|
||||
@@ -694,20 +587,6 @@ const closePreview = () => {
|
||||
previewVisible.value = false
|
||||
previewDocument.value = null
|
||||
}
|
||||
const downloadDocument = (doc: any) => {
|
||||
if (!doc?.path) {
|
||||
return
|
||||
}
|
||||
const target = String(doc.path)
|
||||
if (target.startsWith('data:')) {
|
||||
const link = document.createElement('a')
|
||||
link.href = target
|
||||
link.download = doc.filename || doc.name || 'document'
|
||||
link.click()
|
||||
return
|
||||
}
|
||||
window.open(target, '_blank')
|
||||
}
|
||||
const removeDocument = async (documentId: string | number | null | undefined) => {
|
||||
if (!documentId) {
|
||||
return
|
||||
@@ -848,15 +727,7 @@ watch(structureProducts, (products) => {
|
||||
})
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
customFieldInputs.value.every((field) => {
|
||||
if (!field.required) {
|
||||
return true
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}),
|
||||
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||
)
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
@@ -869,19 +740,6 @@ const canSubmit = computed(() =>
|
||||
),
|
||||
)
|
||||
|
||||
const toFieldString = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const fetchPiece = async () => {
|
||||
const id = route.params.id
|
||||
if (!id || typeof id !== 'string') {
|
||||
@@ -1042,7 +900,15 @@ const submitEdition = async () => {
|
||||
const result = await updatePiece(piece.value.id, payload)
|
||||
if (result.success) {
|
||||
const updatedPiece = result.data
|
||||
await saveCustomFieldValues(updatedPiece)
|
||||
await _saveCustomFieldValues(
|
||||
'piece',
|
||||
updatedPiece.id,
|
||||
[
|
||||
updatedPiece?.typePiece?.pieceCustomFields,
|
||||
updatedPiece?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields,
|
||||
],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
await router.push('/pieces-catalog')
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -1052,271 +918,6 @@ const submitEdition = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildCustomFieldInputs = (
|
||||
structure: PieceModelStructure | null,
|
||||
values: any[] | null,
|
||||
): CustomFieldInput[] => {
|
||||
const definitions = normalizeCustomFieldInputs(structure)
|
||||
const valueList = Array.isArray(values) ? values : []
|
||||
|
||||
const mapById = new Map<string, any>()
|
||||
const mapByName = new Map<string, any>()
|
||||
|
||||
valueList.forEach((entry) => {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return
|
||||
}
|
||||
const fieldId = entry.customField?.id || entry.customFieldId || null
|
||||
if (fieldId) {
|
||||
mapById.set(fieldId, entry)
|
||||
}
|
||||
const fieldName = entry.customField?.name || entry.name || entry.key || null
|
||||
if (fieldName) {
|
||||
mapByName.set(fieldName, entry)
|
||||
}
|
||||
})
|
||||
|
||||
const resolved: CustomFieldInput[] = definitions.map((definition) => {
|
||||
const definitionId = definition.customFieldId || definition.id || null
|
||||
const matched = (definitionId ? mapById.get(definitionId) : null) || mapByName.get(definition.name)
|
||||
|
||||
if (!matched) {
|
||||
return {
|
||||
...definition,
|
||||
customFieldId: definition.customFieldId || definition.id,
|
||||
customFieldValueId: null,
|
||||
orderIndex: definition.orderIndex,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedValue = matched.value ?? ''
|
||||
return {
|
||||
...definition,
|
||||
customFieldId: matched.customField?.id || definition.customFieldId || definition.id,
|
||||
customFieldValueId: matched.id ?? null,
|
||||
value: formatDefaultValue(definition.type, resolvedValue),
|
||||
orderIndex: Math.min(
|
||||
definition.orderIndex ?? 0,
|
||||
typeof matched.customField?.orderIndex === 'number'
|
||||
? matched.customField.orderIndex
|
||||
: definition.orderIndex ?? 0,
|
||||
),
|
||||
}
|
||||
}).sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldValueId || field.id || `${field.name}-${index}`
|
||||
|
||||
const normalizeCustomFieldInputs = (structure: PieceModelStructure | null): CustomFieldInput[] => {
|
||||
if (!structure || typeof structure !== 'object') {
|
||||
return []
|
||||
}
|
||||
const fields = Array.isArray(structure.customFields) ? structure.customFields : []
|
||||
return fields
|
||||
.map((field, index) => normalizeCustomField(field, index))
|
||||
.filter((field): field is CustomFieldInput => field !== null)
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
|
||||
if (!rawField || typeof rawField !== 'object') {
|
||||
return null
|
||||
}
|
||||
const name = resolveFieldName(rawField)
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
const type = resolveFieldType(rawField)
|
||||
const required = !!rawField.required
|
||||
const options = Array.isArray(rawField.options)
|
||||
? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
|
||||
: []
|
||||
const defaultSource = resolveDefaultValue(rawField)
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
|
||||
? rawField.customFieldValueId
|
||||
: null
|
||||
|
||||
const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
|
||||
|
||||
return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex }
|
||||
}
|
||||
|
||||
const resolveFieldName = (field: any): string => {
|
||||
if (typeof field?.name === 'string' && field.name.trim()) {
|
||||
return field.name.trim()
|
||||
}
|
||||
if (typeof field?.key === 'string' && field.key.trim()) {
|
||||
return field.key.trim()
|
||||
}
|
||||
if (typeof field?.label === 'string' && field.label.trim()) {
|
||||
return field.label.trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveFieldType = (field: any): string => {
|
||||
const allowed = ['text', 'number', 'select', 'boolean', 'date']
|
||||
const value = typeof field?.type === 'string' ? field.type.toLowerCase() : ''
|
||||
return allowed.includes(value) ? value : 'text'
|
||||
}
|
||||
|
||||
const resolveDefaultValue = (field: any): any => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return null
|
||||
}
|
||||
if (field.defaultValue !== undefined && field.defaultValue !== null) {
|
||||
return field.defaultValue
|
||||
}
|
||||
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
|
||||
return field.value
|
||||
}
|
||||
if (field.default !== undefined && field.default !== null) {
|
||||
return field.default
|
||||
}
|
||||
if (field.value && typeof field.value === 'object') {
|
||||
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
|
||||
return (field.value as any).defaultValue
|
||||
}
|
||||
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
|
||||
return (field.value as any).value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof defaultValue === 'object') {
|
||||
if (defaultValue === null) {
|
||||
return ''
|
||||
}
|
||||
if ('defaultValue' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
|
||||
}
|
||||
if ('value' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') {
|
||||
return 'true'
|
||||
}
|
||||
if (normalized === 'false' || normalized === '0') {
|
||||
return 'false'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return String(defaultValue)
|
||||
}
|
||||
|
||||
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||
customFieldName: field.name,
|
||||
customFieldType: field.type,
|
||||
customFieldRequired: field.required,
|
||||
customFieldOptions: field.options,
|
||||
})
|
||||
|
||||
const shouldPersistField = (field: CustomFieldInput) => {
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}
|
||||
|
||||
const formatValueForPersistence = (field: CustomFieldInput) => {
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' ? 'true' : 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim()
|
||||
}
|
||||
|
||||
const saveCustomFieldValues = async (updatedPiece: any) => {
|
||||
if (!updatedPiece || !updatedPiece.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const definitionMap = new Map<string, string>()
|
||||
const registerDefinitions = (fields: any[]) => {
|
||||
if (!Array.isArray(fields)) {
|
||||
return
|
||||
}
|
||||
fields.forEach((field) => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return
|
||||
}
|
||||
const name = typeof field.name === 'string' ? field.name : null
|
||||
const id = typeof field.id === 'string' ? field.id : null
|
||||
if (name && id && !definitionMap.has(name)) {
|
||||
definitionMap.set(name, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerDefinitions(updatedPiece?.typePiece?.pieceCustomFields)
|
||||
registerDefinitions(updatedPiece?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields)
|
||||
|
||||
const resolveDefinitionId = (field: CustomFieldInput) => {
|
||||
if (field.customFieldId) {
|
||||
return field.customFieldId
|
||||
}
|
||||
if (field.id) {
|
||||
return field.id
|
||||
}
|
||||
return definitionMap.get(field.name) ?? null
|
||||
}
|
||||
|
||||
for (const field of customFieldInputs.value) {
|
||||
if (!shouldPersistField(field)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const definitionId = resolveDefinitionId(field)
|
||||
const metadata = definitionId ? undefined : buildCustomFieldMetadata(field)
|
||||
const value = formatValueForPersistence(field)
|
||||
|
||||
if (field.customFieldValueId) {
|
||||
const result = await updateCustomFieldValue(field.customFieldValueId, { value })
|
||||
if (!result.success) {
|
||||
toast.showError(`Impossible de mettre à jour le champ personnalisé "${field.name}"`)
|
||||
} else if (definitionId && !field.customFieldId) {
|
||||
field.customFieldId = definitionId
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await upsertCustomFieldValue(
|
||||
definitionId,
|
||||
'piece',
|
||||
updatedPiece.id,
|
||||
value,
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
toast.showError(`Impossible d'enregistrer le champ personnalisé "${field.name}"`)
|
||||
} else {
|
||||
const createdValue = result.data
|
||||
if (createdValue?.id) {
|
||||
field.customFieldValueId = createdValue.id
|
||||
}
|
||||
const resolvedId = createdValue?.customField?.id || definitionId
|
||||
if (resolvedId) {
|
||||
field.customFieldId = resolvedId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.allSettled([loadPieceTypes(), fetchPiece()])
|
||||
loading.value = false
|
||||
|
||||
@@ -308,6 +308,13 @@ import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
||||
import type { ModelType } from '~/services/modelTypes'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
fieldKey,
|
||||
normalizeCustomFieldInputs,
|
||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||
saveCustomFieldValues as _saveCustomFieldValues,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
|
||||
interface PieceCatalogType extends ModelType {
|
||||
structure: PieceModelStructure | null
|
||||
@@ -466,15 +473,7 @@ watch(selectedType, (type) => {
|
||||
})
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
customFieldInputs.value.every((field) => {
|
||||
if (!field.required) {
|
||||
return true
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}),
|
||||
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||
)
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
@@ -487,19 +486,6 @@ const canSubmit = computed(() =>
|
||||
),
|
||||
)
|
||||
|
||||
const toFieldString = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const clearCreationForm = () => {
|
||||
creationForm.name = ''
|
||||
creationForm.reference = ''
|
||||
@@ -558,7 +544,15 @@ const submitCreation = async () => {
|
||||
try {
|
||||
const result = await createPiece(payload)
|
||||
if (result.success) {
|
||||
await saveCustomFieldValues(result.data)
|
||||
await _saveCustomFieldValues(
|
||||
'piece',
|
||||
result.data.id,
|
||||
[
|
||||
result.data?.typePiece?.pieceCustomFields,
|
||||
result.data?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields,
|
||||
],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
if (selectedDocuments.value.length && result.data?.id) {
|
||||
uploadingDocuments.value = true
|
||||
const uploadResult = await uploadDocuments(
|
||||
@@ -593,225 +587,4 @@ onMounted(async () => {
|
||||
await loadPieceTypes()
|
||||
})
|
||||
|
||||
interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
customFieldValueId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldValueId || field.id || `${field.name}-${index}`
|
||||
|
||||
const normalizeCustomFieldInputs = (structure: PieceModelStructure | null): CustomFieldInput[] => {
|
||||
if (!structure || typeof structure !== 'object') {
|
||||
return []
|
||||
}
|
||||
const fields = Array.isArray(structure.customFields) ? structure.customFields : []
|
||||
return fields
|
||||
.map((field, index) => normalizeCustomField(field, index))
|
||||
.filter((field): field is CustomFieldInput => field !== null)
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
|
||||
if (!rawField || typeof rawField !== 'object') {
|
||||
return null
|
||||
}
|
||||
const name = resolveFieldName(rawField)
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
const type = resolveFieldType(rawField)
|
||||
const required = !!rawField.required
|
||||
const options = Array.isArray(rawField.options)
|
||||
? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
|
||||
: []
|
||||
const defaultSource = resolveDefaultValue(rawField)
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
|
||||
? rawField.customFieldValueId
|
||||
: null
|
||||
|
||||
const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
|
||||
|
||||
return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex }
|
||||
}
|
||||
|
||||
const resolveFieldName = (field: any): string => {
|
||||
if (typeof field?.name === 'string' && field.name.trim()) {
|
||||
return field.name.trim()
|
||||
}
|
||||
if (typeof field?.key === 'string' && field.key.trim()) {
|
||||
return field.key.trim()
|
||||
}
|
||||
if (typeof field?.label === 'string' && field.label.trim()) {
|
||||
return field.label.trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveFieldType = (field: any): string => {
|
||||
const allowed = ['text', 'number', 'select', 'boolean', 'date']
|
||||
const value = typeof field?.type === 'string' ? field.type.toLowerCase() : ''
|
||||
return allowed.includes(value) ? value : 'text'
|
||||
}
|
||||
|
||||
const resolveDefaultValue = (field: any): any => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return null
|
||||
}
|
||||
if (field.defaultValue !== undefined && field.defaultValue !== null) {
|
||||
return field.defaultValue
|
||||
}
|
||||
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
|
||||
return field.value
|
||||
}
|
||||
if (field.default !== undefined && field.default !== null) {
|
||||
return field.default
|
||||
}
|
||||
if (field.value && typeof field.value === 'object') {
|
||||
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
|
||||
return (field.value as any).defaultValue
|
||||
}
|
||||
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
|
||||
return (field.value as any).value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (typeof defaultValue === 'object') {
|
||||
if (defaultValue === null) {
|
||||
return ''
|
||||
}
|
||||
if ('defaultValue' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
|
||||
}
|
||||
if ('value' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') {
|
||||
return 'true'
|
||||
}
|
||||
if (normalized === 'false' || normalized === '0') {
|
||||
return 'false'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return String(defaultValue)
|
||||
}
|
||||
|
||||
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
|
||||
customFieldName: field.name,
|
||||
customFieldType: field.type,
|
||||
customFieldRequired: field.required,
|
||||
customFieldOptions: field.options,
|
||||
})
|
||||
|
||||
const saveCustomFieldValues = async (createdPiece: any) => {
|
||||
if (!createdPiece || !createdPiece.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const definitionMap = new Map<string, string>()
|
||||
const registerDefinitions = (fields: any[]) => {
|
||||
if (!Array.isArray(fields)) {
|
||||
return
|
||||
}
|
||||
fields.forEach((field) => {
|
||||
if (!field || typeof field !== 'object') {
|
||||
return
|
||||
}
|
||||
const name = typeof field.name === 'string' ? field.name : null
|
||||
const id = typeof field.id === 'string' ? field.id : null
|
||||
if (name && id && !definitionMap.has(name)) {
|
||||
definitionMap.set(name, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerDefinitions(createdPiece?.typePiece?.pieceCustomFields)
|
||||
registerDefinitions(createdPiece?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields)
|
||||
|
||||
const customizeFieldId = (field: CustomFieldInput) => {
|
||||
if (field.customFieldId) {
|
||||
return field.customFieldId
|
||||
}
|
||||
if (field.id) {
|
||||
return field.id
|
||||
}
|
||||
return definitionMap.get(field.name) ?? null
|
||||
}
|
||||
|
||||
for (const field of customFieldInputs.value) {
|
||||
if (!shouldPersistField(field)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const definitionId = customizeFieldId(field)
|
||||
const metadata = definitionId ? undefined : buildCustomFieldMetadata(field)
|
||||
const value = formatValueForPersistence(field)
|
||||
|
||||
if (field.customFieldValueId) {
|
||||
const result = await updateCustomFieldValue(field.customFieldValueId, { value })
|
||||
if (!result.success) {
|
||||
toast.showError(`Impossible de mettre à jour le champ personnalisé "${field.name}"`)
|
||||
} else if (definitionId && !field.customFieldId) {
|
||||
field.customFieldId = definitionId
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await upsertCustomFieldValue(
|
||||
definitionId,
|
||||
'piece',
|
||||
createdPiece.id,
|
||||
value,
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
toast.showError(`Impossible d'enregistrer le champ personnalisé "${field.name}"`)
|
||||
} else {
|
||||
const createdValue = result.data
|
||||
if (createdValue?.id) {
|
||||
field.customFieldValueId = createdValue.id
|
||||
}
|
||||
const resolvedId = createdValue?.customField?.id || definitionId
|
||||
if (resolvedId) {
|
||||
field.customFieldId = resolvedId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldPersistField = (field: CustomFieldInput) => {
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}
|
||||
|
||||
const formatValueForPersistence = (field: CustomFieldInput) => {
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' ? 'true' : 'false'
|
||||
}
|
||||
return toFieldString(field.value).trim()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -402,20 +402,28 @@ import { formatProductStructurePreview, normalizeProductStructureForSave } from
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import { getModelType } from '~/services/modelTypes'
|
||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||
|
||||
interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
customFieldValueId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
fieldKey,
|
||||
buildCustomFieldInputs,
|
||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||
saveCustomFieldValues as _saveCustomFieldValues,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
import {
|
||||
documentIcon,
|
||||
formatSize,
|
||||
shouldInlinePdf,
|
||||
documentPreviewSrc,
|
||||
documentThumbnailClass,
|
||||
downloadDocument,
|
||||
} from '~/shared/utils/documentDisplayUtils'
|
||||
import {
|
||||
historyActionLabel,
|
||||
formatHistoryDate,
|
||||
formatHistoryValue,
|
||||
historyDiffEntries as _historyDiffEntries,
|
||||
} from '~/shared/utils/historyDisplayUtils'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -458,75 +466,8 @@ const historyFieldLabels: Record<string, string> = {
|
||||
constructeurIds: 'Fournisseurs',
|
||||
}
|
||||
|
||||
const historyActionLabel = (action: string) => {
|
||||
if (action === 'create') {
|
||||
return 'Création'
|
||||
}
|
||||
if (action === 'delete') {
|
||||
return 'Suppression'
|
||||
}
|
||||
return 'Modification'
|
||||
}
|
||||
|
||||
const historyDateFormatter = new Intl.DateTimeFormat('fr-FR', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
const formatHistoryDate = (value: string) => {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
return historyDateFormatter.format(date)
|
||||
}
|
||||
|
||||
const formatHistoryValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '—'
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return '—'
|
||||
}
|
||||
return value.map((item) => formatHistoryValue(item)).join(', ')
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const maybeRecord = value as Record<string, unknown>
|
||||
const name = typeof maybeRecord.name === 'string' ? maybeRecord.name : null
|
||||
const id = typeof maybeRecord.id === 'string' ? maybeRecord.id : null
|
||||
if (name && id) {
|
||||
return `${name} (#${id})`
|
||||
}
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
if (id) {
|
||||
return `#${id}`
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch (error) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const historyDiffEntries = (entry: ProductHistoryEntry) => {
|
||||
const diff = entry.diff ?? {}
|
||||
return Object.entries(diff).map(([field, change]) => {
|
||||
const label = historyFieldLabels[field] ?? field
|
||||
const fromLabel = formatHistoryValue(change?.from)
|
||||
const toLabel = formatHistoryValue(change?.to)
|
||||
return {
|
||||
field,
|
||||
label,
|
||||
fromLabel,
|
||||
toLabel,
|
||||
}
|
||||
})
|
||||
}
|
||||
const historyDiffEntries = (entry: ProductHistoryEntry) =>
|
||||
_historyDiffEntries(entry, historyFieldLabels)
|
||||
|
||||
const refreshCustomFieldInputs = (
|
||||
structureOverride?: ProductModelStructure | null,
|
||||
@@ -545,15 +486,7 @@ const editionForm = reactive({
|
||||
})
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
customFieldInputs.value.every((field) => {
|
||||
if (!field.required) {
|
||||
return true
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return field.value.trim().length > 0
|
||||
}),
|
||||
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||
)
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
@@ -562,60 +495,6 @@ const canSubmit = computed(() =>
|
||||
|
||||
const structurePreview = computed(() => formatProductStructurePreview(structure.value))
|
||||
|
||||
const documentIcon = (doc: any) =>
|
||||
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
||||
|
||||
const formatSize = (size: number | null | undefined) => {
|
||||
if (size === null || size === undefined) {
|
||||
return '—'
|
||||
}
|
||||
if (size === 0) {
|
||||
return '0 B'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
||||
const formatted = size / Math.pow(1024, index)
|
||||
return `${formatted.toFixed(1)} ${units[index]}`
|
||||
}
|
||||
|
||||
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||
|
||||
const shouldInlinePdf = (document: any) => {
|
||||
if (!document || !isPdfDocument(document) || !document.path) {
|
||||
return false
|
||||
}
|
||||
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const appendPdfViewerParams = (src: string) => {
|
||||
if (!src || src.startsWith('data:')) {
|
||||
return src || ''
|
||||
}
|
||||
if (src.includes('#')) {
|
||||
return `${src}&toolbar=0&navpanes=0`
|
||||
}
|
||||
return `${src}#toolbar=0&navpanes=0`
|
||||
}
|
||||
|
||||
const documentPreviewSrc = (document: any) => {
|
||||
if (!document?.path) {
|
||||
return ''
|
||||
}
|
||||
if (isPdfDocument(document)) {
|
||||
return appendPdfViewerParams(document.path)
|
||||
}
|
||||
return document.path
|
||||
}
|
||||
|
||||
const documentThumbnailClass = (document: any) => {
|
||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||
return 'h-24 w-20'
|
||||
}
|
||||
return 'h-16 w-16'
|
||||
}
|
||||
|
||||
const openPreview = (doc: any) => {
|
||||
if (!doc || !canPreviewDocument(doc)) {
|
||||
@@ -630,20 +509,6 @@ const closePreview = () => {
|
||||
previewDocument.value = null
|
||||
}
|
||||
|
||||
const downloadDocument = (doc: any) => {
|
||||
if (!doc?.path) {
|
||||
return
|
||||
}
|
||||
const target = String(doc.path)
|
||||
if (target.startsWith('data:')) {
|
||||
const link = document.createElement('a')
|
||||
link.href = target
|
||||
link.download = doc.filename || doc.name || 'document'
|
||||
link.click()
|
||||
return
|
||||
}
|
||||
window.open(target, '_blank')
|
||||
}
|
||||
|
||||
const loadProduct = async () => {
|
||||
const id = route.params.id
|
||||
@@ -768,86 +633,6 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldValueId || field.customFieldId || `${field.name}-${index}`
|
||||
|
||||
const buildCustomFieldInputs = (
|
||||
productStructure: ProductModelStructure | null,
|
||||
values: any[] | null | undefined,
|
||||
): CustomFieldInput[] => {
|
||||
if (!productStructure || typeof productStructure !== 'object') {
|
||||
return []
|
||||
}
|
||||
const definitions = Array.isArray(productStructure.customFields) ? productStructure.customFields : []
|
||||
const valueList = Array.isArray(values) ? values : []
|
||||
|
||||
const byId = new Map<string, any>()
|
||||
const byName = new Map<string, any>()
|
||||
|
||||
valueList.forEach((entry) => {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return
|
||||
}
|
||||
const fieldId = entry.customField?.id || entry.customFieldId || null
|
||||
if (fieldId) {
|
||||
byId.set(fieldId, entry)
|
||||
}
|
||||
const fieldName = entry.customField?.name || entry.name || entry.key || null
|
||||
if (fieldName) {
|
||||
byName.set(fieldName, entry)
|
||||
}
|
||||
})
|
||||
|
||||
return definitions
|
||||
.map((definition, index) => {
|
||||
const definitionId = definition.customFieldId || definition.id || null
|
||||
const matched = (definitionId ? byId.get(definitionId) : null) || byName.get(definition.name)
|
||||
const type = typeof definition.type === 'string' ? definition.type : 'text'
|
||||
const options = Array.isArray(definition.options) ? definition.options : []
|
||||
const required = !!definition.required
|
||||
const orderIndex = typeof definition.orderIndex === 'number' ? definition.orderIndex : index
|
||||
|
||||
if (!matched) {
|
||||
return {
|
||||
id: definition.id ?? null,
|
||||
name: definition.name,
|
||||
type,
|
||||
required,
|
||||
options,
|
||||
value: '',
|
||||
customFieldId: definition.customFieldId || definition.id || null,
|
||||
customFieldValueId: null,
|
||||
orderIndex,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedValue = matched.value ?? ''
|
||||
return {
|
||||
id: definition.id ?? null,
|
||||
name: definition.name,
|
||||
type,
|
||||
required,
|
||||
options,
|
||||
value: formatDefaultValue(type, resolvedValue),
|
||||
customFieldId: matched.customField?.id || definition.customFieldId || definition.id || null,
|
||||
customFieldValueId: matched.id ?? null,
|
||||
orderIndex,
|
||||
}
|
||||
})
|
||||
.filter((field): field is CustomFieldInput => !!field?.name)
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
const formatDefaultValue = (type: string, value: any): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return String(value === true || String(value).toLowerCase() === 'true')
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const submitEdition = async () => {
|
||||
if (!product.value) {
|
||||
return
|
||||
@@ -875,7 +660,12 @@ const submitEdition = async () => {
|
||||
const result = await updateProduct(product.value.id, payload)
|
||||
if (result.success && result.data?.id) {
|
||||
product.value = result.data
|
||||
const failedFields = await saveCustomFieldValues(result.data.id)
|
||||
const failedFields = await _saveCustomFieldValues(
|
||||
'product',
|
||||
result.data.id,
|
||||
[],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
if (failedFields.length) {
|
||||
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
||||
return
|
||||
@@ -890,46 +680,6 @@ const submitEdition = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveCustomFieldValues = async (productId: string) => {
|
||||
const failed: string[] = []
|
||||
for (const field of customFieldInputs.value) {
|
||||
const value = field.value ?? ''
|
||||
if (field.customFieldValueId) {
|
||||
const result = await updateCustomFieldValue(field.customFieldValueId, { value })
|
||||
if (!result.success) {
|
||||
failed.push(field.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const metadata = field.customFieldId
|
||||
? undefined
|
||||
: { customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required }
|
||||
|
||||
const result = await upsertCustomFieldValue(
|
||||
field.customFieldId,
|
||||
'product',
|
||||
productId,
|
||||
String(value ?? ''),
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
failed.push(field.name)
|
||||
} else {
|
||||
const createdValue = result.data
|
||||
if (createdValue?.id) {
|
||||
field.customFieldValueId = createdValue.id
|
||||
}
|
||||
const resolvedId = createdValue?.customField?.id || field.customFieldId
|
||||
if (resolvedId) {
|
||||
field.customFieldId = resolvedId
|
||||
}
|
||||
}
|
||||
}
|
||||
return failed
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProduct()
|
||||
})
|
||||
|
||||
@@ -249,6 +249,10 @@ import { formatProductStructurePreview, normalizeProductStructureForSave } from
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||
import type { ModelType } from '~/services/modelTypes'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
normalizeCustomFieldInputs as normalizeCustomFieldInputsFromUtils,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
|
||||
interface ProductCatalogType extends ModelType {
|
||||
structure: ProductModelStructure | null
|
||||
@@ -276,17 +280,6 @@ const creationForm = reactive({
|
||||
const selectedDocuments = ref<File[]>([])
|
||||
const uploadingDocuments = ref(false)
|
||||
|
||||
interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||
|
||||
const loadingTypes = computed(() => loadingProductTypes.value)
|
||||
@@ -337,7 +330,7 @@ watch(selectedType, (type) => {
|
||||
if (!creationForm.name) {
|
||||
creationForm.name = type.name
|
||||
}
|
||||
customFieldInputs.value = normalizeCustomFieldInputs(normalizeProductStructureForSave(type.structure))
|
||||
customFieldInputs.value = normalizeCustomFieldInputsFromUtils(normalizeProductStructureForSave(type.structure))
|
||||
})
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
@@ -362,49 +355,6 @@ const canSubmit = computed(() => Boolean(
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldId || field.id || `${field.name}-${index}`
|
||||
|
||||
const normalizeCustomFieldInputs = (structure: ProductModelStructure | null): CustomFieldInput[] => {
|
||||
if (!structure || typeof structure !== 'object') {
|
||||
return []
|
||||
}
|
||||
const fields = Array.isArray(structure.customFields) ? structure.customFields : []
|
||||
return fields
|
||||
.map((field, index) => normalizeCustomField(field, index))
|
||||
.filter((field): field is CustomFieldInput => field !== null)
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
|
||||
if (!rawField || typeof rawField !== 'object') {
|
||||
return null
|
||||
}
|
||||
const name = typeof rawField.name === 'string' ? rawField.name.trim() : ''
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
const type = typeof rawField.type === 'string' ? rawField.type : 'text'
|
||||
const required = !!rawField.required
|
||||
const options = Array.isArray(rawField.options)
|
||||
? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
|
||||
: []
|
||||
const defaultSource = rawField.defaultValue ?? rawField.value ?? rawField.default ?? null
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
|
||||
|
||||
return { id, name, type, required, options, value, customFieldId, orderIndex }
|
||||
}
|
||||
|
||||
const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return String(defaultValue === true || String(defaultValue).toLowerCase() === 'true')
|
||||
}
|
||||
return String(defaultValue)
|
||||
}
|
||||
|
||||
const clearForm = () => {
|
||||
creationForm.name = ''
|
||||
creationForm.reference = ''
|
||||
|
||||
@@ -75,10 +75,10 @@ function resolveBaseUrl() {
|
||||
return runtimeConfig.public.apiBaseUrl || '';
|
||||
}
|
||||
|
||||
function createOptions<T>(options: FetchOptions<T> = {}) {
|
||||
function createOptions(options: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
baseURL: resolveBaseUrl(),
|
||||
credentials: 'include' as const,
|
||||
credentials: 'include',
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const extractRelationId = (value: unknown): string | null => {
|
||||
}
|
||||
if (trimmed.includes('/')) {
|
||||
const parts = trimmed.split('/').filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : null;
|
||||
return parts.length ? (parts[parts.length - 1] ?? null) : null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const toStringId = (value: unknown): string | null => {
|
||||
}
|
||||
if (trimmed.includes('/')) {
|
||||
const parts = trimmed.split('/').filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : null;
|
||||
return parts.length ? (parts[parts.length - 1] ?? null) : null;
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
@@ -138,8 +138,8 @@ const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => {
|
||||
if (!options && typeof field?.optionsText === 'string') {
|
||||
const parsedFromText = field.optionsText
|
||||
.split(/\r?\n/)
|
||||
.map((option) => option.trim())
|
||||
.filter((option) => option.length > 0)
|
||||
.map((option: string) => option.trim())
|
||||
.filter((option: string) => option.length > 0)
|
||||
options = parsedFromText.length ? parsedFromText : undefined
|
||||
}
|
||||
}
|
||||
@@ -917,8 +917,8 @@ const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => {
|
||||
: ''
|
||||
const parsed = rawOptions
|
||||
.split(/\r?\n/)
|
||||
.map((option) => option.trim())
|
||||
.filter((option) => option.length > 0)
|
||||
.map((option: string) => option.trim())
|
||||
.filter((option: string) => option.length > 0)
|
||||
options = parsed.length > 0 ? parsed : undefined
|
||||
}
|
||||
|
||||
@@ -944,7 +944,7 @@ export const normalizePieceStructureForSave = (input: any): PieceModelStructure
|
||||
)
|
||||
return {
|
||||
...Object.fromEntries(restEntries),
|
||||
products: sanitizePieceProducts(source.products),
|
||||
products: sanitizePieceProducts(source.products || []),
|
||||
customFields: sanitizePieceCustomFields(source.customFields),
|
||||
}
|
||||
}
|
||||
@@ -974,7 +974,7 @@ export const hydratePieceStructureForEditor = (input: any): PieceModelStructureF
|
||||
...Object.fromEntries(
|
||||
Object.entries(source).filter(([key]) => key !== 'customFields' && key !== 'products'),
|
||||
),
|
||||
products: hydrateProducts(source.products) as PieceModelProduct[],
|
||||
products: hydrateProducts(source.products || []) as PieceModelProduct[],
|
||||
customFields: hydratePieceCustomFields(source.customFields),
|
||||
}
|
||||
return payload
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface ComponentModelCustomField {
|
||||
id?: string
|
||||
customFieldId?: string
|
||||
orderIndex?: number
|
||||
key?: string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
export interface ComponentModelPiece {
|
||||
@@ -52,6 +54,9 @@ export interface PieceModelCustomField {
|
||||
required: boolean
|
||||
options?: string[]
|
||||
orderIndex?: number
|
||||
key?: string
|
||||
value?: unknown
|
||||
defaultValue?: string | null
|
||||
}
|
||||
|
||||
export interface PieceModelProduct {
|
||||
@@ -184,7 +189,7 @@ const validateStructureNode = (
|
||||
: []
|
||||
|
||||
const subcomponents: ComponentModelStructureNode[] = []
|
||||
rawSubcomponents.forEach((subValue, index) => {
|
||||
rawSubcomponents.forEach((subValue: unknown, index: number) => {
|
||||
const parsed = validateStructureNode(subValue, issues, `${path}.subcomponents[${index}]`)
|
||||
if (parsed) {
|
||||
subcomponents.push(parsed)
|
||||
@@ -260,6 +265,7 @@ export const componentModelStructureValidator = {
|
||||
...node,
|
||||
customFields,
|
||||
pieces,
|
||||
products: ((node as unknown) as Record<string, unknown>).products as ComponentModelProduct[] ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
367
app/shared/utils/customFieldFormUtils.ts
Normal file
367
app/shared/utils/customFieldFormUtils.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Custom field form normalization, merge, and persistence utilities.
|
||||
*
|
||||
* Extracted from pages/component/create.vue, component/[id]/edit.vue,
|
||||
* pieces/create.vue, pieces/[id]/edit.vue, product/[id]/edit.vue.
|
||||
*
|
||||
* Every create/edit page was shipping its own copy of these helpers –
|
||||
* this module unifies them behind a single, entity-agnostic API.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
customFieldValueId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
export interface SaveCustomFieldDeps {
|
||||
customFieldInputs: { value: CustomFieldInput[] }
|
||||
upsertCustomFieldValue: (
|
||||
definitionId: string | null,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
value: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
) => Promise<{ success: boolean; data?: any }>
|
||||
updateCustomFieldValue: (
|
||||
id: string,
|
||||
payload: { value: string },
|
||||
) => Promise<{ success: boolean }>
|
||||
toast: { showError: (msg: string) => void }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Primitive helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const toFieldString = (value: unknown): string => {
|
||||
if (value === null || value === undefined) return ''
|
||||
if (typeof value === 'string') return value
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
||||
return ''
|
||||
}
|
||||
|
||||
export const fieldKey = (field: CustomFieldInput, index: number): string =>
|
||||
field.customFieldValueId || field.id || `${field.name}-${index}`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Field resolution helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const resolveFieldName = (field: any): string => {
|
||||
if (typeof field?.name === 'string' && field.name.trim()) return field.name.trim()
|
||||
if (typeof field?.key === 'string' && field.key.trim()) return field.key.trim()
|
||||
if (typeof field?.label === 'string' && field.label.trim()) return field.label.trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
export const resolveFieldType = (field: any): string => {
|
||||
const allowed = ['text', 'number', 'select', 'boolean', 'date']
|
||||
const rawType =
|
||||
typeof field?.type === 'string'
|
||||
? field.type
|
||||
: typeof field?.value?.type === 'string'
|
||||
? field.value.type
|
||||
: ''
|
||||
const value = rawType.toLowerCase()
|
||||
return allowed.includes(value) ? value : 'text'
|
||||
}
|
||||
|
||||
export const resolveRequiredFlag = (field: any): boolean => {
|
||||
if (typeof field?.required === 'boolean') return field.required
|
||||
const nested = field?.value?.required
|
||||
if (typeof nested === 'boolean') return nested
|
||||
if (typeof nested === 'string') {
|
||||
const normalized = nested.toLowerCase()
|
||||
return normalized === 'true' || normalized === '1'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const resolveOptions = (field: any): string[] => {
|
||||
const sources = [field?.options, field?.value?.options, field?.value?.choices]
|
||||
for (const source of sources) {
|
||||
if (Array.isArray(source)) {
|
||||
const mapped = source
|
||||
.map((option: unknown) => {
|
||||
if (option === null || option === undefined) return ''
|
||||
if (typeof option === 'string') return option.trim()
|
||||
if (typeof option === 'object') {
|
||||
const record = (option || {}) as Record<string, unknown>
|
||||
for (const key of ['value', 'label', 'name']) {
|
||||
const candidate = record[key]
|
||||
if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate.trim()
|
||||
}
|
||||
}
|
||||
const fallback = String(option).trim()
|
||||
return fallback === '[object Object]' ? '' : fallback
|
||||
})
|
||||
.filter((o) => o.length > 0)
|
||||
if (mapped.length) return mapped
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export const resolveDefaultValue = (field: any): any => {
|
||||
if (!field || typeof field !== 'object') return null
|
||||
if (field.defaultValue !== undefined && field.defaultValue !== null) return field.defaultValue
|
||||
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') return field.value
|
||||
if (field.default !== undefined && field.default !== null) return field.default
|
||||
if (field.value && typeof field.value === 'object') {
|
||||
if (field.value.defaultValue !== undefined && field.value.defaultValue !== null) return field.value.defaultValue
|
||||
if (field.value.value !== undefined && field.value.value !== null && typeof field.value.value !== 'object') return field.value.value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
if (defaultValue === null || defaultValue === undefined) return ''
|
||||
if (typeof defaultValue === 'object') {
|
||||
if (defaultValue === null) return ''
|
||||
if ('defaultValue' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
|
||||
}
|
||||
if ('value' in (defaultValue as Record<string, any>)) {
|
||||
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
const normalized = String(defaultValue).toLowerCase()
|
||||
if (normalized === 'true' || normalized === '1') return 'true'
|
||||
if (normalized === 'false' || normalized === '0') return 'false'
|
||||
return ''
|
||||
}
|
||||
return String(defaultValue)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalize a single raw custom-field definition into CustomFieldInput
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
|
||||
if (!rawField || typeof rawField !== 'object') return null
|
||||
const name = resolveFieldName(rawField)
|
||||
if (!name) return null
|
||||
const type = resolveFieldType(rawField)
|
||||
const required = resolveRequiredFlag(rawField)
|
||||
const options = resolveOptions(rawField)
|
||||
const defaultSource = resolveDefaultValue(rawField)
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const customFieldValueId = typeof rawField.customFieldValueId === 'string' ? rawField.customFieldValueId : null
|
||||
const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
|
||||
return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalize ALL custom-field definitions from a structure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const normalizeCustomFieldInputs = (structure: any): CustomFieldInput[] => {
|
||||
if (!structure || typeof structure !== 'object') return []
|
||||
const fields = Array.isArray(structure.customFields) ? structure.customFields : []
|
||||
return fields
|
||||
.map((field: any, index: number) => normalizeCustomField(field, index))
|
||||
.filter((field: CustomFieldInput | null): field is CustomFieldInput => field !== null)
|
||||
.sort((a: CustomFieldInput, b: CustomFieldInput) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract stored value from a persisted custom-field entry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const extractStoredCustomFieldValue = (entry: any): any => {
|
||||
if (entry === null || entry === undefined) return ''
|
||||
if (typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean') return entry
|
||||
if (typeof entry !== 'object') return String(entry)
|
||||
|
||||
const direct = entry.value
|
||||
if (direct !== undefined && direct !== null) {
|
||||
if (typeof direct === 'object') {
|
||||
if (direct === null) return ''
|
||||
if ('value' in direct && direct.value !== undefined && direct.value !== null) return direct.value
|
||||
if ('defaultValue' in direct && direct.defaultValue !== undefined && direct.defaultValue !== null) return direct.defaultValue
|
||||
return ''
|
||||
}
|
||||
return direct
|
||||
}
|
||||
if (entry.defaultValue !== undefined && entry.defaultValue !== null) return entry.defaultValue
|
||||
if (entry.customFieldValue?.value !== undefined && entry.customFieldValue?.value !== null) return entry.customFieldValue.value
|
||||
return ''
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build inputs for edit pages (merge definitions + stored values)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const buildCustomFieldInputs = (
|
||||
structure: any,
|
||||
values: any[] | null | undefined,
|
||||
): CustomFieldInput[] => {
|
||||
const definitions = normalizeCustomFieldInputs(structure)
|
||||
const valueList = Array.isArray(values) ? values : []
|
||||
|
||||
const mapById = new Map<string, any>()
|
||||
const mapByName = new Map<string, any>()
|
||||
|
||||
valueList.forEach((entry) => {
|
||||
if (!entry || typeof entry !== 'object') return
|
||||
const fieldId = entry.customField?.id || entry.customFieldId || null
|
||||
if (fieldId) mapById.set(fieldId, entry)
|
||||
const fieldName = entry.customField?.name || entry.name || entry.key || null
|
||||
if (fieldName) mapByName.set(fieldName, entry)
|
||||
})
|
||||
|
||||
return definitions
|
||||
.map((definition) => {
|
||||
const definitionId = definition.customFieldId || definition.id || null
|
||||
const matched = (definitionId ? mapById.get(definitionId) : null) || mapByName.get(definition.name)
|
||||
|
||||
if (!matched) {
|
||||
return {
|
||||
...definition,
|
||||
customFieldId: definition.customFieldId || definition.id,
|
||||
customFieldValueId: null,
|
||||
orderIndex: definition.orderIndex,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedValue = extractStoredCustomFieldValue(matched)
|
||||
return {
|
||||
...definition,
|
||||
customFieldId: matched.customField?.id || definition.customFieldId || definition.id,
|
||||
customFieldValueId: matched.id ?? null,
|
||||
value: formatDefaultValue(definition.type, resolvedValue),
|
||||
orderIndex: Math.min(
|
||||
definition.orderIndex ?? 0,
|
||||
typeof matched.customField?.orderIndex === 'number'
|
||||
? matched.customField.orderIndex
|
||||
: definition.orderIndex ?? 0,
|
||||
),
|
||||
}
|
||||
})
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const buildCustomFieldMetadata = (field: CustomFieldInput): Record<string, unknown> => ({
|
||||
customFieldName: field.name,
|
||||
customFieldType: field.type,
|
||||
customFieldRequired: field.required,
|
||||
customFieldOptions: field.options,
|
||||
})
|
||||
|
||||
export const shouldPersistField = (field: CustomFieldInput): boolean => {
|
||||
if (field.type === 'boolean') return field.value === 'true' || field.value === 'false'
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
}
|
||||
|
||||
export const formatValueForPersistence = (field: CustomFieldInput): string => {
|
||||
if (field.type === 'boolean') return field.value === 'true' ? 'true' : 'false'
|
||||
return toFieldString(field.value).trim()
|
||||
}
|
||||
|
||||
export const requiredCustomFieldsFilled = (inputs: CustomFieldInput[]): boolean =>
|
||||
inputs.every((field) => {
|
||||
if (!field.required) return true
|
||||
if (field.type === 'boolean') return field.value === 'true' || field.value === 'false'
|
||||
return toFieldString(field.value).trim() !== ''
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Save custom-field values for an entity.
|
||||
*
|
||||
* @param entityType - API entity slug ('composant' | 'piece' | 'product')
|
||||
* @param entityId - ID of the created/updated entity
|
||||
* @param definitionSources - arrays of raw definition objects to build a name→id map
|
||||
* @param deps - injected composable references
|
||||
* @returns list of field names that failed to save (empty = all OK)
|
||||
*/
|
||||
export const saveCustomFieldValues = async (
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
definitionSources: any[][],
|
||||
deps: SaveCustomFieldDeps,
|
||||
): Promise<string[]> => {
|
||||
if (!entityId) return []
|
||||
|
||||
const definitionMap = new Map<string, string>()
|
||||
const registerDefinitions = (fields: any[]) => {
|
||||
if (!Array.isArray(fields)) return
|
||||
fields.forEach((field) => {
|
||||
if (!field || typeof field !== 'object') return
|
||||
const name = typeof field.name === 'string' ? field.name : null
|
||||
const id = typeof field.id === 'string' ? field.id : null
|
||||
if (name && id && !definitionMap.has(name)) definitionMap.set(name, id)
|
||||
})
|
||||
}
|
||||
|
||||
definitionSources.forEach(registerDefinitions)
|
||||
|
||||
const resolveDefinitionId = (field: CustomFieldInput) => {
|
||||
if (field.customFieldId) return field.customFieldId
|
||||
if (field.id) return field.id
|
||||
return definitionMap.get(field.name) ?? null
|
||||
}
|
||||
|
||||
const failed: string[] = []
|
||||
|
||||
for (const field of deps.customFieldInputs.value) {
|
||||
if (!shouldPersistField(field)) continue
|
||||
|
||||
const definitionId = resolveDefinitionId(field)
|
||||
const metadata = definitionId ? undefined : buildCustomFieldMetadata(field)
|
||||
const value = formatValueForPersistence(field)
|
||||
|
||||
if (field.customFieldValueId) {
|
||||
const result = await deps.updateCustomFieldValue(field.customFieldValueId, { value })
|
||||
if (!result.success) {
|
||||
deps.toast.showError(`Impossible de mettre à jour le champ personnalisé "${field.name}"`)
|
||||
failed.push(field.name)
|
||||
} else if (definitionId && !field.customFieldId) {
|
||||
field.customFieldId = definitionId
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await deps.upsertCustomFieldValue(
|
||||
definitionId,
|
||||
entityType,
|
||||
entityId,
|
||||
value,
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
deps.toast.showError(`Impossible d'enregistrer le champ personnalisé "${field.name}"`)
|
||||
failed.push(field.name)
|
||||
} else {
|
||||
const createdValue = result.data
|
||||
if (createdValue?.id) field.customFieldValueId = createdValue.id
|
||||
const resolvedId = createdValue?.customField?.id || definitionId
|
||||
if (resolvedId) field.customFieldId = resolvedId
|
||||
}
|
||||
}
|
||||
|
||||
return failed
|
||||
}
|
||||
@@ -173,6 +173,20 @@ export interface NormalizedCustomFieldDefinition {
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
export interface NormalizedCustomFieldEntry {
|
||||
customFieldValueId: unknown
|
||||
id: string | undefined
|
||||
customFieldId: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
optionsText: string
|
||||
defaultValue: unknown
|
||||
value: string
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
export const normalizeCustomFieldDefinitionEntry = (
|
||||
definition: Record<string, unknown> = {},
|
||||
fallbackIndex = 0,
|
||||
@@ -237,8 +251,8 @@ export const mergeCustomFieldValuesWithDefinitions = (
|
||||
valueEntries: Record<string, unknown>[] = [],
|
||||
...definitionSources: unknown[][]
|
||||
): Record<string, unknown>[] => {
|
||||
const normalizedValues = (Array.isArray(valueEntries) ? valueEntries : [])
|
||||
.map((entry) => {
|
||||
const normalizedValues: Record<string, unknown>[] = (Array.isArray(valueEntries) ? valueEntries : [])
|
||||
.map((entry): Record<string, unknown> | null => {
|
||||
if (!entry || typeof entry !== 'object') return null
|
||||
const normalizedDefinition = normalizeCustomFieldDefinitionEntry(
|
||||
((entry as Record<string, unknown>).customField || entry) as Record<string, unknown>,
|
||||
@@ -265,7 +279,7 @@ export const mergeCustomFieldValuesWithDefinitions = (
|
||||
defaultValue: normalizedDefinition.defaultValue ?? '',
|
||||
value,
|
||||
readOnly: !!(entry as Record<string, unknown>)?.readOnly,
|
||||
}
|
||||
} as Record<string, unknown>
|
||||
})
|
||||
.filter((entry): entry is Record<string, unknown> => entry !== null)
|
||||
|
||||
|
||||
65
app/shared/utils/documentDisplayUtils.ts
Normal file
65
app/shared/utils/documentDisplayUtils.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Document display & preview helpers for edit pages.
|
||||
*
|
||||
* Extracted from pages/component/[id]/edit.vue, pieces/[id]/edit.vue,
|
||||
* product/[id]/edit.vue – each had an identical copy of these utilities.
|
||||
*/
|
||||
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||
|
||||
export const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||
|
||||
export const formatSize = (size: number | null | undefined): string => {
|
||||
if (size === null || size === undefined) return '—'
|
||||
if (size === 0) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
||||
const formatted = size / Math.pow(1024, index)
|
||||
return `${formatted.toFixed(1)} ${units[index]}`
|
||||
}
|
||||
|
||||
export const shouldInlinePdf = (doc: any): boolean => {
|
||||
if (!doc || !isPdfDocument(doc) || !doc.path) return false
|
||||
if (typeof doc.size === 'number' && doc.size > PDF_PREVIEW_MAX_BYTES) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export const appendPdfViewerParams = (src: string): string => {
|
||||
if (!src || src.startsWith('data:')) return src || ''
|
||||
if (src.includes('#')) return `${src}&toolbar=0&navpanes=0`
|
||||
return `${src}#toolbar=0&navpanes=0`
|
||||
}
|
||||
|
||||
export const documentPreviewSrc = (doc: any): string => {
|
||||
if (!doc?.path) return ''
|
||||
if (isPdfDocument(doc)) return appendPdfViewerParams(doc.path)
|
||||
return doc.path
|
||||
}
|
||||
|
||||
export const documentThumbnailClass = (doc: any): string => {
|
||||
if (shouldInlinePdf(doc) || (isImageDocument(doc) && doc?.path)) return 'h-24 w-20'
|
||||
return 'h-16 w-16'
|
||||
}
|
||||
|
||||
export interface FileIconResult {
|
||||
component: unknown
|
||||
colorClass: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export const documentIcon = (doc: any): FileIconResult =>
|
||||
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
||||
|
||||
export const downloadDocument = (doc: any): void => {
|
||||
if (!doc?.path) return
|
||||
const target = String(doc.path)
|
||||
if (target.startsWith('data:')) {
|
||||
const link = document.createElement('a')
|
||||
link.href = target
|
||||
link.download = doc.filename || doc.name || 'document'
|
||||
link.click()
|
||||
return
|
||||
}
|
||||
window.open(target, '_blank')
|
||||
}
|
||||
78
app/shared/utils/historyDisplayUtils.ts
Normal file
78
app/shared/utils/historyDisplayUtils.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* History display utilities for edit pages.
|
||||
*
|
||||
* Extracted from pages/component/[id]/edit.vue, pieces/[id]/edit.vue,
|
||||
* product/[id]/edit.vue – each had an identical copy.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Formatters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const historyActionLabel = (action: string): string => {
|
||||
if (action === 'create') return 'Création'
|
||||
if (action === 'delete') return 'Suppression'
|
||||
return 'Modification'
|
||||
}
|
||||
|
||||
const historyDateFormatter = new Intl.DateTimeFormat('fr-FR', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
export const formatHistoryDate = (value: string): string => {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return historyDateFormatter.format(date)
|
||||
}
|
||||
|
||||
export const formatHistoryValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return '—'
|
||||
return value.map((item) => formatHistoryValue(item)).join(', ')
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const maybeRecord = value as Record<string, unknown>
|
||||
const name = typeof maybeRecord.name === 'string' ? maybeRecord.name : null
|
||||
const id = typeof maybeRecord.id === 'string' ? maybeRecord.id : null
|
||||
if (name && id) return `${name} (#${id})`
|
||||
if (name) return name
|
||||
if (id) return `#${id}`
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diff entries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface DiffChange {
|
||||
from?: unknown
|
||||
to?: unknown
|
||||
}
|
||||
|
||||
export interface HistoryDiffEntry {
|
||||
field: string
|
||||
label: string
|
||||
fromLabel: string
|
||||
toLabel: string
|
||||
}
|
||||
|
||||
export const historyDiffEntries = (
|
||||
entry: { diff?: Record<string, DiffChange> | null },
|
||||
fieldLabels: Record<string, string>,
|
||||
): HistoryDiffEntry[] => {
|
||||
const diff = entry.diff ?? {}
|
||||
return Object.entries(diff).map(([field, change]) => ({
|
||||
field,
|
||||
label: fieldLabels[field] ?? field,
|
||||
fromLabel: formatHistoryValue(change?.from),
|
||||
toLabel: formatHistoryValue(change?.to),
|
||||
}))
|
||||
}
|
||||
14
app/types/icons.d.ts
vendored
Normal file
14
app/types/icons.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// Type declarations for unplugin-icons
|
||||
// This allows TypeScript to understand icon imports from ~icons/*
|
||||
|
||||
declare module '~icons/*' {
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
const component: FunctionalComponent<SVGAttributes>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '~icons/lucide/*' {
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
const component: FunctionalComponent<SVGAttributes>
|
||||
export default component
|
||||
}
|
||||
41
app/utils/events.ts
Normal file
41
app/utils/events.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Type-safe event handlers for form inputs.
|
||||
* These helpers extract values from DOM events in a way that satisfies TypeScript.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extract the string value from an input event.
|
||||
*/
|
||||
export const getInputValue = (event: Event): string => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
return target?.value ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the numeric value from an input event.
|
||||
*/
|
||||
export const getInputNumber = (event: Event): number => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
const value = target?.value ?? ''
|
||||
const parsed = parseFloat(value)
|
||||
return Number.isNaN(parsed) ? 0 : parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the checked state from a checkbox event.
|
||||
*/
|
||||
export const getCheckboxValue = (event: Event): boolean => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
return target?.checked ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an optional numeric value from an input event (empty string = undefined).
|
||||
*/
|
||||
export const getOptionalNumber = (event: Event): number | undefined => {
|
||||
const target = event.target as HTMLInputElement | null
|
||||
const value = target?.value ?? ''
|
||||
if (value.trim() === '') return undefined
|
||||
const parsed = parseFloat(value)
|
||||
return Number.isNaN(parsed) ? undefined : parsed
|
||||
}
|
||||
@@ -20,8 +20,10 @@ export const maskEmail = (rawValue: string): string => {
|
||||
return ''
|
||||
}
|
||||
|
||||
const [localPart, domain] = value.split('@')
|
||||
if (!domain) {
|
||||
const parts = value.split('@')
|
||||
const localPart = parts[0] ?? ''
|
||||
const domain = parts[1]
|
||||
if (!domain || !localPart) {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -29,7 +31,7 @@ export const maskEmail = (rawValue: string): string => {
|
||||
return `${localPart[0] ?? ''}·@${domain}`
|
||||
}
|
||||
|
||||
const start = localPart[0]
|
||||
const start = localPart[0] ?? ''
|
||||
const end = localPart.slice(-1)
|
||||
const masked = '·'.repeat(Math.max(0, localPart.length - 2))
|
||||
|
||||
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"@iconify-json/lucide": "^1.2.68",
|
||||
"@nuxt/eslint-config": "^1.9.0",
|
||||
"@rushstack/eslint-patch": "^1.12.0",
|
||||
"@types/node": "^25.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||
"@typescript-eslint/parser": "^8.44.1",
|
||||
"eslint": "^9.36.0",
|
||||
@@ -92,7 +93,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2087,7 +2087,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -4054,6 +4053,16 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz",
|
||||
"integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-path": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
||||
@@ -4102,7 +4111,6 @@
|
||||
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.44.1",
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
@@ -4784,7 +4792,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
||||
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
@@ -5004,7 +5011,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5422,7 +5428,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -6782,7 +6787,6 @@
|
||||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -10094,7 +10098,6 @@
|
||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
@@ -10141,7 +10144,6 @@
|
||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.87.0.tgz",
|
||||
"integrity": "sha512-uc47XrtHwkBoES4HFgwgfH9sqwAtJXgAIBq4fFBMZ4hWmgVZoExyn+L4g4VuaecVKXkz1bvlaHcfwHAJPQb5Gw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "^0.87.0"
|
||||
},
|
||||
@@ -10496,7 +10498,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -10936,7 +10937,6 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -12481,7 +12481,6 @@
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -12701,7 +12700,6 @@
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -12749,6 +12747,13 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
"version": "2.0.0-rc.21",
|
||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz",
|
||||
@@ -13052,7 +13057,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
@@ -13299,7 +13303,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -13625,7 +13628,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
@@ -13663,7 +13665,6 @@
|
||||
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"eslint-scope": "^8.2.0",
|
||||
@@ -13687,7 +13688,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@iconify-json/lucide": "^1.2.68",
|
||||
"@nuxt/eslint-config": "^1.9.0",
|
||||
"@rushstack/eslint-patch": "^1.12.0",
|
||||
"@types/node": "^25.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||
"@typescript-eslint/parser": "^8.44.1",
|
||||
"eslint": "^9.36.0",
|
||||
|
||||
Reference in New Issue
Block a user