Compare commits
4 Commits
v1.2.0
...
9ee348fff0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ee348fff0 | ||
|
|
1fbd1d1b2e | ||
|
|
1f2d6c78e8 | ||
|
|
649f8ca9cc |
@@ -246,6 +246,16 @@ const emitSelection = (ids: string[]) => {
|
|||||||
emit('update:modelValue', normalized)
|
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) => {
|
const ensureOptionsLoaded = async (force = false) => {
|
||||||
if (!force && !searchTerm.value && constructeurs.value.length) {
|
if (!force && !searchTerm.value && constructeurs.value.length) {
|
||||||
applyOptions(constructeurs.value as ConstructeurSummary[])
|
applyOptions(constructeurs.value as ConstructeurSummary[])
|
||||||
@@ -262,7 +272,7 @@ const ensureOptionsLoaded = async (force = false) => {
|
|||||||
|
|
||||||
const result = await searchConstructeurs(searchTerm.value)
|
const result = await searchConstructeurs(searchTerm.value)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
applyOptions(result.data || [])
|
applyOptions(extractDataArray(result.data))
|
||||||
lastSearchTerm = searchTerm.value
|
lastSearchTerm = searchTerm.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,7 +293,7 @@ const onSearch = () => {
|
|||||||
}
|
}
|
||||||
const result = await searchConstructeurs(searchTerm.value)
|
const result = await searchConstructeurs(searchTerm.value)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
applyOptions(result.data || [])
|
applyOptions(extractDataArray(result.data))
|
||||||
lastSearchTerm = searchTerm.value
|
lastSearchTerm = searchTerm.value
|
||||||
}
|
}
|
||||||
}, 250)
|
}, 250)
|
||||||
@@ -310,16 +320,18 @@ const closeCreateModal = () => {
|
|||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
creating.value = true
|
creating.value = true
|
||||||
const payload = { ...createForm.value }
|
const payload: { name: string; email?: string; phone?: string } = {
|
||||||
if (!payload.phone) {
|
name: createForm.value.name,
|
||||||
delete payload.phone
|
|
||||||
}
|
}
|
||||||
if (!payload.email) {
|
if (createForm.value.email) {
|
||||||
delete payload.email
|
payload.email = createForm.value.email
|
||||||
|
}
|
||||||
|
if (createForm.value.phone) {
|
||||||
|
payload.phone = createForm.value.phone
|
||||||
}
|
}
|
||||||
const result = await createConstructeur(payload)
|
const result = await createConstructeur(payload)
|
||||||
creating.value = false
|
creating.value = false
|
||||||
if (result.success) {
|
if (result.success && result.data && !Array.isArray(result.data)) {
|
||||||
emitSelection([...selectedIds.value, result.data.id])
|
emitSelection([...selectedIds.value, result.data.id])
|
||||||
searchTerm.value = ''
|
searchTerm.value = ''
|
||||||
closeCreateModal()
|
closeCreateModal()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
:model-value="modelValue"
|
:model-value="modelValue ?? undefined"
|
||||||
:options="productOptions"
|
:options="productOptions"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
|
|||||||
@@ -825,10 +825,9 @@ const customFieldReorderClass = (index: number) => {
|
|||||||
|
|
||||||
const addCustomField = () => {
|
const addCustomField = () => {
|
||||||
ensureArray('customFields')
|
ensureArray('customFields')
|
||||||
const nextIndex = Array.isArray(props.node.customFields)
|
const fields = props.node.customFields!
|
||||||
? props.node.customFields.length
|
const nextIndex = fields.length
|
||||||
: 0
|
fields.push({
|
||||||
props.node.customFields.push({
|
|
||||||
name: '',
|
name: '',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: false,
|
required: false,
|
||||||
@@ -847,7 +846,7 @@ const removeCustomField = (index: number) => {
|
|||||||
|
|
||||||
const addPiece = () => {
|
const addPiece = () => {
|
||||||
ensureArray('pieces')
|
ensureArray('pieces')
|
||||||
props.node.pieces.push({
|
props.node.pieces!.push({
|
||||||
typePieceId: '',
|
typePieceId: '',
|
||||||
typePieceLabel: '',
|
typePieceLabel: '',
|
||||||
reference: '',
|
reference: '',
|
||||||
@@ -863,7 +862,7 @@ const removePiece = (index: number) => {
|
|||||||
|
|
||||||
const addProduct = () => {
|
const addProduct = () => {
|
||||||
ensureArray('products')
|
ensureArray('products')
|
||||||
props.node.products.push({
|
props.node.products!.push({
|
||||||
typeProductId: '',
|
typeProductId: '',
|
||||||
typeProductLabel: '',
|
typeProductLabel: '',
|
||||||
familyCode: '',
|
familyCode: '',
|
||||||
@@ -911,6 +910,7 @@ const moveItemInPlace = <T,>(list: T[], from: number, to: number) => {
|
|||||||
}
|
}
|
||||||
const updated = list.slice()
|
const updated = list.slice()
|
||||||
const [item] = updated.splice(from, 1)
|
const [item] = updated.splice(from, 1)
|
||||||
|
if (item === undefined) return
|
||||||
updated.splice(to, 0, item)
|
updated.splice(to, 0, item)
|
||||||
list.splice(0, list.length, ...updated)
|
list.splice(0, list.length, ...updated)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered input-sm"
|
class="input input-bordered input-sm"
|
||||||
:placeholder="labels.labelPlaceholder"
|
:placeholder="labels.labelPlaceholder"
|
||||||
@input="updateRequirement(index, { label: $event.target.value })"
|
@input="handleLabelInput(index, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
class="input input-bordered input-sm"
|
class="input input-bordered input-sm"
|
||||||
@input="updateRequirement(index, { minCount: parseNumber($event.target.value) })"
|
@input="handleMinInput(index, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
class="input input-bordered input-sm"
|
class="input input-bordered input-sm"
|
||||||
@input="updateRequirement(index, { maxCount: parseOptionalNumber($event.target.value) })"
|
@input="handleMaxInput(index, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-sm"
|
class="checkbox checkbox-sm"
|
||||||
:checked="(requirement.required ?? requiredFallback) === true"
|
:checked="(requirement.required ?? requiredFallback) === true"
|
||||||
@change="updateRequirement(index, { required: $event.target.checked })"
|
@change="handleRequiredChange(index, $event)"
|
||||||
/>
|
/>
|
||||||
{{ labels.requiredLabel }}
|
{{ labels.requiredLabel }}
|
||||||
</label>
|
</label>
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-sm"
|
class="checkbox checkbox-sm"
|
||||||
:checked="(requirement.allowNewModels ?? allowNewModelsFallback) === true"
|
:checked="(requirement.allowNewModels ?? allowNewModelsFallback) === true"
|
||||||
@change="updateRequirement(index, { allowNewModels: $event.target.checked })"
|
@change="handleAllowNewModelsChange(index, $event)"
|
||||||
/>
|
/>
|
||||||
{{ labels.allowNewModelsLabel }}
|
{{ labels.allowNewModelsLabel }}
|
||||||
</label>
|
</label>
|
||||||
@@ -277,6 +277,37 @@ const parseOptionalNumber = (value: string) => {
|
|||||||
return Number.isFinite(parsed) ? parsed : null
|
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 draggingRequirementIndex = ref<number | null>(null)
|
||||||
const requirementDropTargetIndex = 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 updated = list.slice() as Requirement[]
|
||||||
const [moved] = updated.splice(from, 1)
|
const [moved] = updated.splice(from, 1)
|
||||||
|
if (!moved) {
|
||||||
|
resetRequirementDragState()
|
||||||
|
return
|
||||||
|
}
|
||||||
updated.splice(to, 0, moved)
|
updated.splice(to, 0, moved)
|
||||||
requirements.value = applyOrderIndex(updated)
|
requirements.value = applyOrderIndex(updated)
|
||||||
resetRequirementDragState()
|
resetRequirementDragState()
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
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)
|
const loadingComponentTypes = ref(false)
|
||||||
|
|
||||||
export function useComponentTypes () {
|
export function useComponentTypes() {
|
||||||
const { showSuccess, showError } = useToast()
|
const { showSuccess, showError } = useToast()
|
||||||
|
|
||||||
const generateCodeFromName = (name) => {
|
const generateCodeFromName = (name: string): string => {
|
||||||
return (name || '')
|
return (name || '')
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/[\u0300-\u036F]/g, '')
|
.replace(/[\u0300-\u036F]/g, '')
|
||||||
@@ -18,24 +38,26 @@ export function useComponentTypes () {
|
|||||||
.replace(/-+/g, '-') || 'type'
|
.replace(/-+/g, '-') || 'type'
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadComponentTypes = async () => {
|
const loadComponentTypes = async (): Promise<ComponentTypeResult> => {
|
||||||
loadingComponentTypes.value = true
|
loadingComponentTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await listModelTypes({
|
const data = await listModelTypes({
|
||||||
category: 'COMPONENT',
|
category: 'COMPONENT',
|
||||||
sort: 'name',
|
sort: 'name',
|
||||||
dir: 'asc',
|
dir: 'asc',
|
||||||
limit: 200
|
limit: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
componentTypes.value = data.items.map(item => ({
|
componentTypes.value = data.items.map((item) => ({
|
||||||
...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 }
|
return { success: true, data: componentTypes.value }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Impossible de charger les types de composant: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -43,21 +65,22 @@ export function useComponentTypes () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createComponentType = async (payload) => {
|
const createComponentType = async (payload: ComponentTypePayload): Promise<ComponentTypeResult> => {
|
||||||
loadingComponentTypes.value = true
|
loadingComponentTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await createModelType({
|
const data = await createModelType({
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
code: payload.code || generateCodeFromName(payload.name),
|
code: payload.code || generateCodeFromName(payload.name),
|
||||||
category: 'COMPONENT',
|
category: 'COMPONENT',
|
||||||
notes: payload.description ?? payload.notes,
|
notes: payload.description ?? payload.notes ?? undefined,
|
||||||
description: payload.description ?? null,
|
description: payload.description ?? undefined,
|
||||||
structure: payload.structure
|
structure: payload.structure ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalized = {
|
const normalized: ComponentType = {
|
||||||
...data,
|
...data,
|
||||||
description: data.description ?? data.notes ?? null
|
structure: data.structure as ComponentModelStructure | null,
|
||||||
|
description: data.description ?? data.notes ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
componentTypes.value.push(normalized)
|
componentTypes.value.push(normalized)
|
||||||
@@ -65,7 +88,8 @@ export function useComponentTypes () {
|
|||||||
|
|
||||||
return { success: true, data: normalized }
|
return { success: true, data: normalized }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la création du type de composant: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} 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
|
loadingComponentTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await updateModelType(id, {
|
const data = await updateModelType(id, {
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
description: payload.description,
|
description: payload.description ?? undefined,
|
||||||
notes: payload.notes,
|
notes: payload.notes ?? undefined,
|
||||||
code: payload.code,
|
code: payload.code,
|
||||||
structure: payload.structure
|
structure: payload.structure ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalized = {
|
const normalized: ComponentType = {
|
||||||
...data,
|
...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) {
|
if (index !== -1) {
|
||||||
componentTypes.value[index] = normalized
|
componentTypes.value[index] = normalized
|
||||||
}
|
}
|
||||||
showSuccess(`Type de composant "${data.name}" mis à jour`)
|
showSuccess(`Type de composant "${data.name}" mis à jour`)
|
||||||
|
|
||||||
return {
|
return { success: true, data: normalized }
|
||||||
success: true,
|
|
||||||
data: normalized
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la mise à jour du type de composant: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -108,15 +131,16 @@ export function useComponentTypes () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteComponentType = async (id) => {
|
const deleteComponentType = async (id: string): Promise<ComponentTypeResult> => {
|
||||||
loadingComponentTypes.value = true
|
loadingComponentTypes.value = true
|
||||||
try {
|
try {
|
||||||
await deleteModelType(id)
|
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é')
|
showSuccess('Type de composant supprimé')
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la suppression du type de composant: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -135,6 +159,6 @@ export function useComponentTypes () {
|
|||||||
updateComponentType,
|
updateComponentType,
|
||||||
deleteComponentType,
|
deleteComponentType,
|
||||||
getComponentTypes,
|
getComponentTypes,
|
||||||
isComponentTypeLoading
|
isComponentTypeLoading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,45 +2,83 @@ import { ref } from 'vue'
|
|||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs } from './useConstructeurs'
|
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
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 total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const extractCollection = (payload) => {
|
const extractCollection = (payload: unknown): Composant[] => {
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) {
|
||||||
return payload
|
return payload as Composant[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.member)) {
|
const p = payload as Record<string, unknown> | null
|
||||||
return payload.member
|
if (Array.isArray(p?.member)) {
|
||||||
|
return p.member as Composant[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.['hydra:member'])) {
|
if (Array.isArray(p?.['hydra:member'])) {
|
||||||
return payload['hydra:member']
|
return p['hydra:member'] as Composant[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.data)) {
|
if (Array.isArray(p?.data)) {
|
||||||
return payload.data
|
return p.data as Composant[]
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractTotal = (payload, fallbackLength) => {
|
const extractTotal = (payload: unknown, fallbackLength: number): number => {
|
||||||
if (typeof payload?.totalItems === 'number') {
|
const p = payload as Record<string, unknown> | null
|
||||||
return payload.totalItems
|
if (typeof p?.totalItems === 'number') {
|
||||||
|
return p.totalItems
|
||||||
}
|
}
|
||||||
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
if (typeof p?.['hydra:totalItems'] === 'number') {
|
||||||
return payload['hydra:totalItems']
|
return p['hydra:totalItems']
|
||||||
}
|
}
|
||||||
return fallbackLength
|
return fallbackLength
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useComposants () {
|
export function useComposants() {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const withResolvedConstructeurs = async (composant) => {
|
const withResolvedConstructeurs = async (composant: Composant): Promise<Composant> => {
|
||||||
if (!composant || typeof composant !== 'object') {
|
if (!composant || typeof composant !== 'object') {
|
||||||
return composant
|
return composant
|
||||||
}
|
}
|
||||||
@@ -59,12 +97,11 @@ export function useComposants () {
|
|||||||
const ids = uniqueConstructeurIds(
|
const ids = uniqueConstructeurIds(
|
||||||
composant.constructeurIds,
|
composant.constructeurIds,
|
||||||
composant.constructeurs,
|
composant.constructeurs,
|
||||||
composant.constructeur,
|
|
||||||
)
|
)
|
||||||
const hasResolvedConstructeurs =
|
const hasResolvedConstructeurs =
|
||||||
Array.isArray(composant.constructeurs)
|
Array.isArray(composant.constructeurs) &&
|
||||||
&& composant.constructeurs.length > 0
|
composant.constructeurs.length > 0 &&
|
||||||
&& composant.constructeurs.every((item) => item && typeof item === 'object')
|
composant.constructeurs.every((item) => item && typeof item === 'object')
|
||||||
|
|
||||||
if (ids.length && !hasResolvedConstructeurs) {
|
if (ids.length && !hasResolvedConstructeurs) {
|
||||||
const resolved = await ensureConstructeurs(ids)
|
const resolved = await ensureConstructeurs(ids)
|
||||||
@@ -76,16 +113,7 @@ export function useComposants () {
|
|||||||
return composant
|
return composant
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const loadComposants = async (options: LoadComposantsOptions = {}): Promise<ComposantListResult> => {
|
||||||
* 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 = {}) => {
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -93,7 +121,7 @@ export function useComposants () {
|
|||||||
page = 1,
|
page = 1,
|
||||||
itemsPerPage = 30,
|
itemsPerPage = 30,
|
||||||
orderBy = 'name',
|
orderBy = 'name',
|
||||||
orderDir = 'asc'
|
orderDir = 'asc',
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -118,79 +146,84 @@ export function useComposants () {
|
|||||||
items: enrichedItems,
|
items: enrichedItems,
|
||||||
total: total.value,
|
total: total.value,
|
||||||
page,
|
page,
|
||||||
itemsPerPage
|
itemsPerPage,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result as ComposantListResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement des composants:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createComposant = async (composantData) => {
|
const createComposant = async (composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
||||||
const result = await post('/composants', normalizedPayload)
|
const result = await post('/composants', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data as Composant)
|
||||||
composants.value.unshift(enriched)
|
composants.value.unshift(enriched)
|
||||||
total.value += 1
|
total.value += 1
|
||||||
const displayName = result.data?.name
|
const definition = (composantData as Record<string, unknown>)?.definition as Record<string, unknown> | undefined
|
||||||
|| composantData?.definition?.name
|
const displayName =
|
||||||
|| composantData?.name
|
(result.data as Composant)?.name ||
|
||||||
|| 'Composant'
|
(definition?.name as string | undefined) ||
|
||||||
|
composantData?.name ||
|
||||||
|
'Composant'
|
||||||
showSuccess(`Composant "${displayName}" créé avec succès`)
|
showSuccess(`Composant "${displayName}" créé avec succès`)
|
||||||
|
return { success: true, data: enriched }
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la création du composant:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateComposantData = async (id, composantData) => {
|
const updateComposantData = async (id: string, composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData))
|
||||||
const result = await patch(`/composants/${id}`, normalizedPayload)
|
const result = await patch(`/composants/${id}`, normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
const updated = await withResolvedConstructeurs(result.data)
|
const updated = await withResolvedConstructeurs(result.data as Composant)
|
||||||
const index = composants.value.findIndex(comp => comp.id === id)
|
const index = composants.value.findIndex((comp) => comp.id === id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
composants.value[index] = updated
|
composants.value[index] = updated
|
||||||
}
|
}
|
||||||
showSuccess(`Composant "${updated?.name || composantData.name || ''}" mis à jour avec succès`)
|
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) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la mise à jour du composant:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteComposant = async (id) => {
|
const deleteComposant = async (id: string): Promise<ComposantSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await del(`/composants/${id}`)
|
const result = await del(`/composants/${id}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const deletedComposant = composants.value.find(comp => comp.id === id)
|
const deletedComposant = composants.value.find((comp) => comp.id === id)
|
||||||
composants.value = composants.value.filter(comp => comp.id !== id)
|
composants.value = composants.value.filter((comp) => comp.id !== id)
|
||||||
total.value = Math.max(0, total.value - 1)
|
total.value = Math.max(0, total.value - 1)
|
||||||
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
|
showSuccess(`Composant "${deletedComposant?.name || 'inconnu'}" supprimé avec succès`)
|
||||||
|
return { success: true }
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la suppression du composant:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -208,6 +241,6 @@ export function useComposants () {
|
|||||||
updateComposant: updateComposantData,
|
updateComposant: updateComposantData,
|
||||||
deleteComposant,
|
deleteComposant,
|
||||||
getComposants,
|
getComposants,
|
||||||
isLoading
|
isLoading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,24 @@ import { ref } from 'vue'
|
|||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { useToast } from './useToast'
|
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 loading = ref(false)
|
||||||
|
|
||||||
const uniqueConstructeurs = (items = []) => {
|
const uniqueConstructeurs = (items: Constructeur[] = []): Constructeur[] => {
|
||||||
const map = new Map()
|
const map = new Map<string, Constructeur>()
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
if (item && typeof item === 'object' && typeof item.id === 'string') {
|
if (item && typeof item === 'object' && typeof item.id === 'string') {
|
||||||
map.set(item.id, item)
|
map.set(item.id, item)
|
||||||
@@ -15,7 +28,7 @@ const uniqueConstructeurs = (items = []) => {
|
|||||||
return Array.from(map.values())
|
return Array.from(map.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeIds = (ids = []) => {
|
const normalizeIds = (ids: unknown[] = []): string[] => {
|
||||||
if (!Array.isArray(ids)) {
|
if (!Array.isArray(ids)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -28,7 +41,7 @@ const normalizeIds = (ids = []) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertConstructeurs = (items = []) => {
|
const upsertConstructeurs = (items: Constructeur[] = []) => {
|
||||||
if (!Array.isArray(items) || !items.length) {
|
if (!Array.isArray(items) || !items.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -36,32 +49,33 @@ const upsertConstructeurs = (items = []) => {
|
|||||||
constructeurs.value = merged
|
constructeurs.value = merged
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIndexedConstructeur = (id) =>
|
const getIndexedConstructeur = (id: string): Constructeur | null =>
|
||||||
constructeurs.value.find((item) => item && item.id === id) || null
|
constructeurs.value.find((item) => item && item.id === id) || null
|
||||||
|
|
||||||
const extractCollection = (payload) => {
|
const extractCollection = (payload: unknown): Constructeur[] => {
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) {
|
||||||
return payload
|
return payload as Constructeur[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.member)) {
|
const p = payload as Record<string, unknown> | null
|
||||||
return payload.member
|
if (Array.isArray(p?.member)) {
|
||||||
|
return p.member as Constructeur[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.['hydra:member'])) {
|
if (Array.isArray(p?.['hydra:member'])) {
|
||||||
return payload['hydra:member']
|
return p['hydra:member'] as Constructeur[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.data)) {
|
if (Array.isArray(p?.data)) {
|
||||||
return payload.data
|
return p.data as Constructeur[]
|
||||||
}
|
}
|
||||||
return []
|
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 { get, post, patch, delete: del } = useApi()
|
||||||
const { showSuccess, showError } = useToast()
|
const { showSuccess, showError } = useToast()
|
||||||
|
|
||||||
const loadConstructeurs = async (search = '') => {
|
const loadConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
||||||
@@ -70,47 +84,49 @@ export function useConstructeurs () {
|
|||||||
const items = extractCollection(result.data)
|
const items = extractCollection(result.data)
|
||||||
constructeurs.value = uniqueConstructeurs(items)
|
constructeurs.value = uniqueConstructeurs(items)
|
||||||
}
|
}
|
||||||
return result
|
return result as ConstructeurResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const err = error as Error
|
||||||
console.error('Erreur lors du chargement des fournisseurs:', error)
|
console.error('Erreur lors du chargement des fournisseurs:', error)
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: err.message }
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchConstructeurs = async (search = '') => {
|
const searchConstructeurs = async (search = ''): Promise<ConstructeurResult> => {
|
||||||
return loadConstructeurs(search)
|
return loadConstructeurs(search)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createConstructeur = async (data) => {
|
const createConstructeur = async (data: Partial<Constructeur>): Promise<ConstructeurResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await post('/constructeurs', data)
|
const result = await post('/constructeurs', data)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
upsertConstructeurs([result.data])
|
upsertConstructeurs([result.data as Constructeur])
|
||||||
showSuccess(`Fournisseur "${result.data.name}" créé`)
|
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" créé`)
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
}
|
}
|
||||||
return result
|
return result as ConstructeurResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const err = error as Error
|
||||||
console.error('Erreur lors de la création du fournisseur:', error)
|
console.error('Erreur lors de la création du fournisseur:', error)
|
||||||
showError('Impossible de créer le fournisseur')
|
showError('Impossible de créer le fournisseur')
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: err.message }
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureConstructeurs = async (ids = []) => {
|
const ensureConstructeurs = async (ids: unknown[] = []): Promise<Constructeur[]> => {
|
||||||
const normalizedIds = normalizeIds(ids)
|
const normalizedIds = normalizeIds(ids)
|
||||||
if (!normalizedIds.length) {
|
if (!normalizedIds.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const collected = []
|
const collected: Constructeur[] = []
|
||||||
const missing = []
|
const missing: string[] = []
|
||||||
normalizedIds.forEach((id) => {
|
normalizedIds.forEach((id) => {
|
||||||
const existing = getIndexedConstructeur(id)
|
const existing = getIndexedConstructeur(id)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -129,7 +145,7 @@ export function useConstructeurs () {
|
|||||||
const task = get(`/constructeurs/${id}`)
|
const task = get(`/constructeurs/${id}`)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
return result.data
|
return result.data as Constructeur
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -145,7 +161,7 @@ export function useConstructeurs () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fetched = await Promise.all(fetchTasks)
|
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) {
|
if (validFetched.length) {
|
||||||
upsertConstructeurs(validFetched)
|
upsertConstructeurs(validFetched)
|
||||||
}
|
}
|
||||||
@@ -153,50 +169,52 @@ export function useConstructeurs () {
|
|||||||
|
|
||||||
return normalizedIds
|
return normalizedIds
|
||||||
.map((id) => getIndexedConstructeur(id))
|
.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
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/constructeurs/${id}`, data)
|
const result = await patch(`/constructeurs/${id}`, data)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
upsertConstructeurs([result.data])
|
upsertConstructeurs([result.data as Constructeur])
|
||||||
showSuccess(`Fournisseur "${result.data.name}" mis à jour`)
|
showSuccess(`Fournisseur "${(result.data as Constructeur).name}" mis à jour`)
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
}
|
}
|
||||||
return result
|
return result as ConstructeurResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const err = error as Error
|
||||||
console.error('Erreur lors de la mise à jour du fournisseur:', error)
|
console.error('Erreur lors de la mise à jour du fournisseur:', error)
|
||||||
showError('Impossible de mettre à jour le fournisseur')
|
showError('Impossible de mettre à jour le fournisseur')
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: err.message }
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteConstructeur = async (id) => {
|
const deleteConstructeur = async (id: string): Promise<ConstructeurResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await del(`/constructeurs/${id}`)
|
const result = await del(`/constructeurs/${id}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
constructeurs.value = constructeurs.value.filter(item => item.id !== id)
|
constructeurs.value = constructeurs.value.filter((item) => item.id !== id)
|
||||||
showSuccess('Fournisseur supprimé')
|
showSuccess('Fournisseur supprimé')
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
}
|
}
|
||||||
return result
|
return result as ConstructeurResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const err = error as Error
|
||||||
console.error('Erreur lors de la suppression du fournisseur:', error)
|
console.error('Erreur lors de la suppression du fournisseur:', error)
|
||||||
showError('Impossible de supprimer le fournisseur')
|
showError('Impossible de supprimer le fournisseur')
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: err.message }
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConstructeurById = (id) => getIndexedConstructeur(id)
|
const getConstructeurById = (id: string) => getIndexedConstructeur(id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
constructeurs,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
572
app/composables/useMachineCreatePreview.ts
Normal file
572
app/composables/useMachineCreatePreview.ts
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
/**
|
||||||
|
* Machine creation – preview computation and validation.
|
||||||
|
*
|
||||||
|
* Extracted from pages/machines/new.vue. Builds the live preview model
|
||||||
|
* and validates requirement selections before machine creation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, type Ref, type ComputedRef } from 'vue'
|
||||||
|
import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
|
||||||
|
import { extractParentLinkIdentifiers } from '~/shared/utils/productDisplayUtils'
|
||||||
|
import {
|
||||||
|
getComponentMachineAssignments,
|
||||||
|
getPieceMachineAssignments,
|
||||||
|
getPieceComponentAssignments,
|
||||||
|
formatAssignmentList,
|
||||||
|
} from '~/shared/utils/assignmentUtils'
|
||||||
|
|
||||||
|
type AnyRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
export interface MachineCreatePreviewDeps {
|
||||||
|
newMachine: { name: string; siteId: string; typeMachineId: string; reference: string }
|
||||||
|
sites: Ref<AnyRecord[]>
|
||||||
|
selectedMachineType: ComputedRef<AnyRecord | null>
|
||||||
|
findComponentById: (id: string) => AnyRecord | null
|
||||||
|
findPieceById: (id: string) => AnyRecord | null
|
||||||
|
findProductById: (id: string) => AnyRecord | null
|
||||||
|
getComponentRequirementEntries: (requirementId: string) => AnyRecord[]
|
||||||
|
getPieceRequirementEntries: (requirementId: string) => AnyRecord[]
|
||||||
|
getProductRequirementEntries: (requirementId: string) => AnyRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Product type ID extractors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const getProductTypeIdFromComponent = (component: AnyRecord | null): string | null => {
|
||||||
|
if (!component || typeof component !== 'object') return null
|
||||||
|
return (
|
||||||
|
(component.product as AnyRecord)?.typeProductId ||
|
||||||
|
((component.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
|
||||||
|
component.productTypeId ||
|
||||||
|
null
|
||||||
|
) as string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProductTypeIdFromPiece = (piece: AnyRecord | null): string | null => {
|
||||||
|
if (!piece || typeof piece !== 'object') return null
|
||||||
|
return (
|
||||||
|
(piece.product as AnyRecord)?.typeProductId ||
|
||||||
|
((piece.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
|
||||||
|
piece.productTypeId ||
|
||||||
|
null
|
||||||
|
) as string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Status badge helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const getStatusBadgeClass = (status: string): string => {
|
||||||
|
if (status === 'ready') return 'badge-success'
|
||||||
|
if (status === 'warning') return 'badge-warning'
|
||||||
|
return 'badge-error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Scroll / issue click helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const highlightClasses = ['ring', 'ring-primary', 'ring-offset-2']
|
||||||
|
|
||||||
|
export const scrollToAnchor = (anchor: string): void => {
|
||||||
|
if (!anchor || typeof window === 'undefined' || typeof document === 'undefined') return
|
||||||
|
const target = document.getElementById(anchor)
|
||||||
|
if (!target) return
|
||||||
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
highlightClasses.forEach((cls) => target.classList.add(cls))
|
||||||
|
window.setTimeout(() => {
|
||||||
|
highlightClasses.forEach((cls) => target.classList.remove(cls))
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleIssueClick = (issue: AnyRecord): void => {
|
||||||
|
if (!issue?.anchor) return
|
||||||
|
scrollToAnchor(issue.anchor as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Type label resolvers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const resolveComponentRequirementTypeLabel = (
|
||||||
|
requirement: AnyRecord,
|
||||||
|
entry: AnyRecord,
|
||||||
|
findComponentById: (id: string) => AnyRecord | null,
|
||||||
|
): string => {
|
||||||
|
if (entry?.composantId) {
|
||||||
|
const component = findComponentById(entry.composantId as string)
|
||||||
|
if ((component?.typeComposant as AnyRecord)?.name) {
|
||||||
|
return (component!.typeComposant as AnyRecord).name as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ((requirement?.typeComposant as AnyRecord)?.name as string) || 'Type non défini'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolvePieceRequirementTypeLabel = (
|
||||||
|
requirement: AnyRecord,
|
||||||
|
entry: AnyRecord,
|
||||||
|
findPieceById: (id: string) => AnyRecord | null,
|
||||||
|
): string => {
|
||||||
|
if (entry?.pieceId) {
|
||||||
|
const piece = findPieceById(entry.pieceId as string)
|
||||||
|
if ((piece?.typePiece as AnyRecord)?.name) {
|
||||||
|
return (piece!.typePiece as AnyRecord).name as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ((requirement?.typePiece as AnyRecord)?.name as string) || 'Type non défini'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Product requirement stats
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const computeProductUsageFromSelections = (
|
||||||
|
type: AnyRecord,
|
||||||
|
deps: MachineCreatePreviewDeps,
|
||||||
|
): Map<string, number> => {
|
||||||
|
const usage = new Map<string, number>()
|
||||||
|
|
||||||
|
const increment = (typeProductId: string | null) => {
|
||||||
|
if (!typeProductId) return
|
||||||
|
usage.set(typeProductId, (usage.get(typeProductId) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const requirement of (type.componentRequirements || []) as AnyRecord[]) {
|
||||||
|
const entries = deps.getComponentRequirementEntries(requirement.id as string)
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry?.composantId) return
|
||||||
|
const component = deps.findComponentById(entry.composantId as string)
|
||||||
|
increment(getProductTypeIdFromComponent(component))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const requirement of (type.pieceRequirements || []) as AnyRecord[]) {
|
||||||
|
const entries = deps.getPieceRequirementEntries(requirement.id as string)
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry?.pieceId) return
|
||||||
|
const piece = deps.findPieceById(entry.pieceId as string)
|
||||||
|
increment(getProductTypeIdFromPiece(piece))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const requirement of (type.productRequirements || []) as AnyRecord[]) {
|
||||||
|
const entries = deps.getProductRequirementEntries(requirement.id as string)
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry?.productId) return
|
||||||
|
const product = deps.findProductById(entry.productId as string)
|
||||||
|
const typeProductId = (
|
||||||
|
product?.typeProductId ||
|
||||||
|
(product?.typeProduct as AnyRecord)?.id ||
|
||||||
|
entry?.typeProductId ||
|
||||||
|
requirement?.typeProductId ||
|
||||||
|
(requirement?.typeProduct as AnyRecord)?.id ||
|
||||||
|
null
|
||||||
|
) as string | null
|
||||||
|
increment(typeProductId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return usage
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildProductRequirementStats = (
|
||||||
|
type: AnyRecord,
|
||||||
|
deps: MachineCreatePreviewDeps,
|
||||||
|
): { stats: AnyRecord[]; usage: Map<string, number> } => {
|
||||||
|
const usage = computeProductUsageFromSelections(type, deps)
|
||||||
|
|
||||||
|
const stats = ((type.productRequirements || []) as AnyRecord[]).map((requirement) => {
|
||||||
|
const typeProductId = (
|
||||||
|
requirement.typeProductId || (requirement.typeProduct as AnyRecord)?.id || null
|
||||||
|
) as string | null
|
||||||
|
|
||||||
|
const label = (
|
||||||
|
(requirement.label as string)?.trim() ||
|
||||||
|
(requirement.typeProduct as AnyRecord)?.name ||
|
||||||
|
(requirement.typeProduct as AnyRecord)?.code ||
|
||||||
|
'Produit requis'
|
||||||
|
) as string
|
||||||
|
|
||||||
|
const typeName = ((requirement.typeProduct as AnyRecord)?.name || 'Non défini') as string
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
const count = typeProductId ? usage.get(typeProductId) ?? 0 : 0
|
||||||
|
|
||||||
|
const rawEntries = deps.getProductRequirementEntries(requirement.id as string)
|
||||||
|
const normalizedEntries = rawEntries.map((entry, index) => {
|
||||||
|
const product = entry?.productId ? deps.findProductById(entry.productId as string) : null
|
||||||
|
const subtitleParts: string[] = []
|
||||||
|
if (product?.reference) subtitleParts.push(`Réf. ${product.reference}`)
|
||||||
|
if (product?.supplierPrice !== undefined && product?.supplierPrice !== null) {
|
||||||
|
const price = Number(product.supplierPrice)
|
||||||
|
if (!Number.isNaN(price)) subtitleParts.push(`${price.toFixed(2)} €`)
|
||||||
|
}
|
||||||
|
if (Array.isArray(product?.constructeurs) && (product!.constructeurs as AnyRecord[]).length) {
|
||||||
|
const cLabel = (product!.constructeurs as AnyRecord[])
|
||||||
|
.map((c) => c?.name)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')
|
||||||
|
if (cLabel) subtitleParts.push(`Fournisseurs: ${cLabel}`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: `${requirement.id}-${index}`,
|
||||||
|
status: product ? 'complete' : 'pending',
|
||||||
|
title: (product?.name || product?.reference || `Sélection #${index + 1}`) as string,
|
||||||
|
subtitle: subtitleParts.length ? subtitleParts.join(' • ') : null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const issues: AnyRecord[] = []
|
||||||
|
if (count < min) {
|
||||||
|
issues.push({
|
||||||
|
message: `Le produit "${label}" nécessite au moins ${min} sélection(s). Actuellement ${count}.`,
|
||||||
|
kind: 'error',
|
||||||
|
anchor: `product-group-${requirement.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (max !== null && count > max) {
|
||||||
|
issues.push({
|
||||||
|
message: `Le produit "${label}" ne peut pas dépasser ${max} sélection(s). Actuellement ${count}.`,
|
||||||
|
kind: 'error',
|
||||||
|
anchor: `product-group-${requirement.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (normalizedEntries.length > 0 && normalizedEntries.some((e) => e.status !== 'complete')) {
|
||||||
|
issues.push({
|
||||||
|
message: 'Sélectionner un produit pour chaque entrée ajoutée.',
|
||||||
|
kind: 'error',
|
||||||
|
anchor: `product-group-${requirement.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = normalizedEntries.filter((e) => e.status === 'complete').length
|
||||||
|
const total = normalizedEntries.length
|
||||||
|
const status = issues.some((i) => i.kind === 'error')
|
||||||
|
? 'error'
|
||||||
|
: issues.some((i) => i.kind === 'warning')
|
||||||
|
? 'warning'
|
||||||
|
: 'ready'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: requirement.id,
|
||||||
|
requirement,
|
||||||
|
label,
|
||||||
|
typeName,
|
||||||
|
count,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
completed,
|
||||||
|
total,
|
||||||
|
entries: normalizedEntries,
|
||||||
|
issues,
|
||||||
|
allowNewModels: requirement.allowNewModels ?? true,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { stats, usage }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Validation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const validateRequirementSelections = (
|
||||||
|
type: AnyRecord,
|
||||||
|
deps: MachineCreatePreviewDeps,
|
||||||
|
): AnyRecord => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const componentLinksPayload: AnyRecord[] = []
|
||||||
|
const pieceLinksPayload: AnyRecord[] = []
|
||||||
|
const productLinksPayload: AnyRecord[] = []
|
||||||
|
|
||||||
|
for (const requirement of (type.componentRequirements || []) as AnyRecord[]) {
|
||||||
|
const entries = deps.getComponentRequirementEntries(requirement.id as string)
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
|
||||||
|
if (entries.length < min) {
|
||||||
|
errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" nécessite au moins ${min} élément(s).`)
|
||||||
|
}
|
||||||
|
if (max !== null && entries.length > max) {
|
||||||
|
errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" ne peut dépasser ${max} élément(s).`)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.composantId) {
|
||||||
|
errors.push(`Sélectionner un composant existant pour "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}".`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const component = deps.findComponentById(entry.composantId as string)
|
||||||
|
if (!component) {
|
||||||
|
errors.push(`Le composant sélectionné est introuvable (ID: ${entry.composantId}).`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const requiredTypeId = (requirement.typeComposantId || (requirement.typeComposant as AnyRecord)?.id || null) as string | null
|
||||||
|
if (requiredTypeId && component.typeComposantId && component.typeComposantId !== requiredTypeId) {
|
||||||
|
errors.push(`Le composant "${component.name || component.id}" n'appartient pas à la famille attendue.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload: AnyRecord = { requirementId: requirement.id, composantId: entry.composantId }
|
||||||
|
const overrides = sanitizeDefinitionOverrides(entry.definition as AnyRecord)
|
||||||
|
if (overrides) payload.overrides = overrides
|
||||||
|
Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
|
||||||
|
componentLinksPayload.push(payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const requirement of (type.pieceRequirements || []) as AnyRecord[]) {
|
||||||
|
const entries = deps.getPieceRequirementEntries(requirement.id as string)
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
|
||||||
|
if (entries.length < min) {
|
||||||
|
errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" nécessite au moins ${min} élément(s).`)
|
||||||
|
}
|
||||||
|
if (max !== null && entries.length > max) {
|
||||||
|
errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" ne peut dépasser ${max} élément(s).`)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.pieceId) {
|
||||||
|
errors.push(`Sélectionner une pièce existante pour "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}".`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const piece = deps.findPieceById(entry.pieceId as string)
|
||||||
|
if (!piece) {
|
||||||
|
errors.push(`La pièce sélectionnée est introuvable (ID: ${entry.pieceId}).`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const requiredTypeId = (requirement.typePieceId || (requirement.typePiece as AnyRecord)?.id || null) as string | null
|
||||||
|
if (requiredTypeId && piece.typePieceId && piece.typePieceId !== requiredTypeId) {
|
||||||
|
errors.push(`La pièce "${piece.name || piece.id}" n'appartient pas à la famille attendue.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload: AnyRecord = { requirementId: requirement.id, pieceId: entry.pieceId }
|
||||||
|
const overrides = sanitizeDefinitionOverrides(entry.definition as AnyRecord)
|
||||||
|
if (overrides) payload.overrides = overrides
|
||||||
|
Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
|
||||||
|
pieceLinksPayload.push(payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stats: productStats } = buildProductRequirementStats(type, deps)
|
||||||
|
for (const requirement of (type.productRequirements || []) as AnyRecord[]) {
|
||||||
|
const entries = deps.getProductRequirementEntries(requirement.id as string)
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
|
||||||
|
if (max !== null && entries.length > max) {
|
||||||
|
errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" ne peut dépasser ${max} entrée(s) directe(s).`)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.productId) {
|
||||||
|
errors.push(`Sélectionner un produit pour "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}".`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const product = deps.findProductById(entry.productId as string)
|
||||||
|
if (!product) {
|
||||||
|
errors.push(`Le produit sélectionné est introuvable (ID: ${entry.productId}).`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const requiredTypeId = (requirement.typeProductId || (requirement.typeProduct as AnyRecord)?.id || null) as string | null
|
||||||
|
const productTypeId = (product.typeProductId || (product.typeProduct as AnyRecord)?.id || entry.typeProductId || null) as string | null
|
||||||
|
if (requiredTypeId && productTypeId && productTypeId !== requiredTypeId) {
|
||||||
|
errors.push(`Le produit "${product.name || product.reference || product.id}" n'appartient pas à la catégorie attendue.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload: AnyRecord = { requirementId: requirement.id, productId: entry.productId }
|
||||||
|
Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
|
||||||
|
productLinksPayload.push(payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
productStats.forEach((stat) => {
|
||||||
|
((stat.issues || []) as AnyRecord[])
|
||||||
|
.filter((issue) => issue.kind === 'error')
|
||||||
|
.forEach((issue) => errors.push(issue.message as string))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (errors.length > 0) return { valid: false, error: errors[0] }
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
componentLinks: componentLinksPayload,
|
||||||
|
pieceLinks: pieceLinksPayload,
|
||||||
|
productLinks: productLinksPayload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main preview composable
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function useMachineCreatePreview(deps: MachineCreatePreviewDeps) {
|
||||||
|
const machinePreview = computed(() => {
|
||||||
|
const type = deps.selectedMachineType.value
|
||||||
|
if (!type) return null
|
||||||
|
|
||||||
|
const trimmedName = (deps.newMachine.name || '').trim()
|
||||||
|
const currentSite = deps.newMachine.siteId
|
||||||
|
? deps.sites.value.find((site) => site.id === deps.newMachine.siteId) || null
|
||||||
|
: null
|
||||||
|
const trimmedReference = (deps.newMachine.reference || '').trim()
|
||||||
|
|
||||||
|
const baseFields = [
|
||||||
|
{ key: 'name', label: 'Nom', display: trimmedName || 'À renseigner', status: trimmedName ? 'complete' : 'missing' },
|
||||||
|
{ key: 'site', label: 'Site', display: (currentSite?.name || 'Sélectionner un site') as string, status: currentSite ? 'complete' : 'missing' },
|
||||||
|
{ key: 'type', label: 'Type sélectionné', display: type.name as string, status: 'complete' },
|
||||||
|
{ key: 'reference', label: 'Référence', display: trimmedReference || 'Non renseignée', status: trimmedReference ? 'complete' : 'optional' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const baseIssues: AnyRecord[] = []
|
||||||
|
if (!trimmedName) baseIssues.push({ message: 'Renseigner un nom de machine.', kind: 'error', anchor: 'machine-field-name' })
|
||||||
|
if (!currentSite) baseIssues.push({ message: "Sélectionner un site d'affectation.", kind: 'error', anchor: 'machine-field-site' })
|
||||||
|
|
||||||
|
const baseStatus = baseIssues.some((issue) => issue.kind === 'error') ? 'error' : 'ready'
|
||||||
|
|
||||||
|
// Component groups
|
||||||
|
const componentGroups = ((type.componentRequirements || []) as AnyRecord[]).map((requirement) => {
|
||||||
|
const entries = deps.getComponentRequirementEntries(requirement.id as string)
|
||||||
|
const normalizedEntries = entries.map((entry, index) => {
|
||||||
|
const selectedComponent = entry.composantId ? deps.findComponentById(entry.composantId as string) : null
|
||||||
|
const displayName = (selectedComponent?.name || (requirement.typeComposant as AnyRecord)?.name || 'Composant') as string
|
||||||
|
const subtitleParts: string[] = []
|
||||||
|
if (selectedComponent?.reference) subtitleParts.push(`Réf. ${selectedComponent.reference}`)
|
||||||
|
const constructeurName = (selectedComponent?.constructeur as AnyRecord)?.name || selectedComponent?.constructeurName
|
||||||
|
if (constructeurName) subtitleParts.push(constructeurName as string)
|
||||||
|
const machineAssignments = selectedComponent ? getComponentMachineAssignments(selectedComponent) : []
|
||||||
|
const assignmentLabel = formatAssignmentList(machineAssignments)
|
||||||
|
if (assignmentLabel) subtitleParts.push(`Liée à ${assignmentLabel}`)
|
||||||
|
return {
|
||||||
|
key: `${requirement.id}-${index}`,
|
||||||
|
status: entry.composantId ? 'complete' : 'pending',
|
||||||
|
title: displayName,
|
||||||
|
subtitle: subtitleParts.join(' • ') || null,
|
||||||
|
assignmentLabel,
|
||||||
|
assignments: machineAssignments,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
const completed = normalizedEntries.filter((e) => e.status === 'complete').length
|
||||||
|
const issues: AnyRecord[] = []
|
||||||
|
if (entries.length < min) issues.push({ message: `Minimum ${min} sélection(s) requise(s)`, kind: 'error', anchor: `component-group-${requirement.id}` })
|
||||||
|
if (max !== null && entries.length > max) issues.push({ message: `Maximum ${max} dépassé`, kind: 'error', anchor: `component-group-${requirement.id}` })
|
||||||
|
if (normalizedEntries.some((e) => e.status !== 'complete')) issues.push({ message: 'Sélectionner un composant pour chaque entrée.', kind: 'error', anchor: `component-group-${requirement.id}` })
|
||||||
|
|
||||||
|
const hasErrors = issues.some((i) => i.kind === 'error')
|
||||||
|
const hasWarnings = completed < entries.length
|
||||||
|
const status = hasErrors ? 'error' : hasWarnings ? 'warning' : 'ready'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: requirement.id,
|
||||||
|
label: (requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Famille de composants') as string,
|
||||||
|
typeName: ((requirement.typeComposant as AnyRecord)?.name || 'Non défini') as string,
|
||||||
|
min, max, entries: normalizedEntries, issues, completed, total: entries.length, status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Piece groups
|
||||||
|
const pieceGroups = ((type.pieceRequirements || []) as AnyRecord[]).map((requirement) => {
|
||||||
|
const entries = deps.getPieceRequirementEntries(requirement.id as string)
|
||||||
|
const normalizedEntries = entries.map((entry, index) => {
|
||||||
|
const selectedPiece = entry.pieceId ? deps.findPieceById(entry.pieceId as string) : null
|
||||||
|
const displayName = (selectedPiece?.name || (requirement.typePiece as AnyRecord)?.name || 'Pièce') as string
|
||||||
|
const subtitleParts: string[] = []
|
||||||
|
if (selectedPiece?.reference) subtitleParts.push(`Réf. ${selectedPiece.reference}`)
|
||||||
|
const constructeurName = (selectedPiece?.constructeur as AnyRecord)?.name || selectedPiece?.constructeurName
|
||||||
|
if (constructeurName) subtitleParts.push(constructeurName as string)
|
||||||
|
const machineAssignments = selectedPiece ? getPieceMachineAssignments(selectedPiece) : []
|
||||||
|
const machineAssignmentLabel = formatAssignmentList(machineAssignments)
|
||||||
|
if (machineAssignmentLabel) subtitleParts.push(`Machines: ${machineAssignmentLabel}`)
|
||||||
|
const componentAssignments = selectedPiece ? getPieceComponentAssignments(selectedPiece) : []
|
||||||
|
const componentAssignmentLabel = formatAssignmentList(componentAssignments)
|
||||||
|
if (componentAssignmentLabel) subtitleParts.push(`Composants: ${componentAssignmentLabel}`)
|
||||||
|
return {
|
||||||
|
key: `${requirement.id}-${index}`,
|
||||||
|
status: entry.pieceId ? 'complete' : 'pending',
|
||||||
|
title: displayName,
|
||||||
|
subtitle: subtitleParts.join(' • ') || null,
|
||||||
|
machineAssignmentLabel, componentAssignmentLabel,
|
||||||
|
machineAssignments, componentAssignments,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
const completed = normalizedEntries.filter((e) => e.status === 'complete').length
|
||||||
|
const issues: AnyRecord[] = []
|
||||||
|
if (entries.length < min) issues.push({ message: `Minimum ${min} sélection(s) requise(s)`, kind: 'error', anchor: `piece-group-${requirement.id}` })
|
||||||
|
if (max !== null && entries.length > max) issues.push({ message: `Maximum ${max} dépassé`, kind: 'error', anchor: `piece-group-${requirement.id}` })
|
||||||
|
if (normalizedEntries.some((e) => e.status !== 'complete')) issues.push({ message: 'Sélectionner une pièce pour chaque entrée.', kind: 'error', anchor: `piece-group-${requirement.id}` })
|
||||||
|
|
||||||
|
const hasErrors = issues.some((i) => i.kind === 'error')
|
||||||
|
const hasWarnings = completed < entries.length
|
||||||
|
const status = hasErrors ? 'error' : hasWarnings ? 'warning' : 'ready'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: requirement.id,
|
||||||
|
label: (requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Groupe de pièces') as string,
|
||||||
|
typeName: ((requirement.typePiece as AnyRecord)?.name || 'Non défini') as string,
|
||||||
|
min, max, entries: normalizedEntries, issues, completed, total: entries.length, status,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Product groups
|
||||||
|
const { stats: productGroups } = buildProductRequirementStats(type, deps)
|
||||||
|
|
||||||
|
// Aggregate
|
||||||
|
const aggregatedIssues = [
|
||||||
|
...baseIssues.map((issue) => ({ ...issue, scope: 'Informations générales' })),
|
||||||
|
...componentGroups.flatMap((group) => group.issues.map((issue) => ({ ...issue, scope: group.label }))),
|
||||||
|
...pieceGroups.flatMap((group) => group.issues.map((issue) => ({ ...issue, scope: group.label }))),
|
||||||
|
...productGroups.flatMap((group: AnyRecord) => ((group.issues || []) as AnyRecord[]).map((issue) => ({ ...issue, scope: group.label }))),
|
||||||
|
]
|
||||||
|
|
||||||
|
const statuses = [
|
||||||
|
baseStatus,
|
||||||
|
...componentGroups.map((g) => g.status),
|
||||||
|
...pieceGroups.map((g) => g.status),
|
||||||
|
...productGroups.map((g: AnyRecord) => g.status as string),
|
||||||
|
]
|
||||||
|
|
||||||
|
const overallStatus = statuses.includes('error') ? 'error' : statuses.includes('warning') ? 'warning' : 'ready'
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: { fields: baseFields, issues: baseIssues, status: baseStatus },
|
||||||
|
componentGroups,
|
||||||
|
pieceGroups,
|
||||||
|
productGroups,
|
||||||
|
type: {
|
||||||
|
name: type.name,
|
||||||
|
category: type.category || null,
|
||||||
|
hasStructuredDefinition:
|
||||||
|
((type.componentRequirements as unknown[])?.length || 0) > 0 ||
|
||||||
|
((type.pieceRequirements as unknown[])?.length || 0) > 0 ||
|
||||||
|
((type.productRequirements as unknown[])?.length || 0) > 0,
|
||||||
|
},
|
||||||
|
status: overallStatus,
|
||||||
|
ready: overallStatus === 'ready',
|
||||||
|
issues: aggregatedIssues,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const blockingPreviewIssues = computed(() => {
|
||||||
|
if (!machinePreview.value) return []
|
||||||
|
return (machinePreview.value.issues as AnyRecord[]).filter((issue) => issue.kind === 'error')
|
||||||
|
})
|
||||||
|
|
||||||
|
const canCreateMachine = computed(() => {
|
||||||
|
if (!machinePreview.value) return false
|
||||||
|
return blockingPreviewIssues.value.length === 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
machinePreview,
|
||||||
|
blockingPreviewIssues,
|
||||||
|
canCreateMachine,
|
||||||
|
}
|
||||||
|
}
|
||||||
371
app/composables/useMachineCreateSelections.ts
Normal file
371
app/composables/useMachineCreateSelections.ts
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* Machine creation – requirement selection state management.
|
||||||
|
*
|
||||||
|
* Extracted from pages/machines/new.vue. Manages the reactive selection state
|
||||||
|
* for component / piece / product requirements when creating a new machine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
|
||||||
|
type AnyRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
export interface MachineCreateSelectionsDeps {
|
||||||
|
findComponentById: (id: string) => AnyRecord | null
|
||||||
|
findPieceById: (id: string) => AnyRecord | null
|
||||||
|
pieces: { value: AnyRecord[] }
|
||||||
|
get: (url: string) => Promise<AnyRecord>
|
||||||
|
toast: { showError: (msg: string) => void }
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractCollection = (payload: unknown): unknown[] => {
|
||||||
|
if (Array.isArray(payload)) return payload
|
||||||
|
if (Array.isArray((payload as AnyRecord)?.member)) return (payload as AnyRecord).member as unknown[]
|
||||||
|
if (Array.isArray((payload as AnyRecord)?.['hydra:member'])) return (payload as AnyRecord)['hydra:member'] as unknown[]
|
||||||
|
if (Array.isArray((payload as AnyRecord)?.data)) return (payload as AnyRecord).data as unknown[]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMachineCreateSelections(deps: MachineCreateSelectionsDeps) {
|
||||||
|
const { findComponentById, findPieceById, pieces, get, toast } = deps
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Reactive state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const componentRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
|
||||||
|
const pieceRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
|
||||||
|
const productRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
|
||||||
|
|
||||||
|
const pieceOptionsByKey = ref<Record<string, AnyRecord[]>>({})
|
||||||
|
const pieceLoadingByKey = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Piece option caching
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const getPieceKey = (requirement: AnyRecord, entryIndex: number): string =>
|
||||||
|
`${requirement?.id || 'req'}:${entryIndex}`
|
||||||
|
|
||||||
|
const findPieceInCachedOptions = (id: string): AnyRecord | null => {
|
||||||
|
if (!id) return null
|
||||||
|
const buckets = Object.values(pieceOptionsByKey.value || {})
|
||||||
|
for (const bucket of buckets) {
|
||||||
|
if (!Array.isArray(bucket)) continue
|
||||||
|
const found = bucket.find((piece) => piece?.id === id)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachePieceIfMissing = (piece: AnyRecord): void => {
|
||||||
|
if (!piece?.id) return
|
||||||
|
const current = Array.isArray(pieces.value) ? pieces.value : []
|
||||||
|
if (current.some((p: AnyRecord) => p?.id === piece.id)) return
|
||||||
|
pieces.value = [...current, piece]
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPieceOptions = async (
|
||||||
|
requirement: AnyRecord,
|
||||||
|
entryIndex: number,
|
||||||
|
term = '',
|
||||||
|
): Promise<void> => {
|
||||||
|
const key = getPieceKey(requirement, entryIndex)
|
||||||
|
if (pieceLoadingByKey.value[key]) return
|
||||||
|
|
||||||
|
const requirementTypeId =
|
||||||
|
(requirement?.typePieceId || (requirement?.typePiece as AnyRecord)?.id || null) as string | null
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('itemsPerPage', '50')
|
||||||
|
if (term && term.trim()) params.set('name', term.trim())
|
||||||
|
if (requirementTypeId) params.set('typePiece', `/api/model_types/${requirementTypeId}`)
|
||||||
|
|
||||||
|
pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: true }
|
||||||
|
try {
|
||||||
|
const result = await get(`/pieces?${params.toString()}`)
|
||||||
|
if (result.success) {
|
||||||
|
pieceOptionsByKey.value = {
|
||||||
|
...pieceOptionsByKey.value,
|
||||||
|
[key]: extractCollection(result.data) as AnyRecord[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Entry getters
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const getComponentRequirementEntries = (requirementId: string): AnyRecord[] =>
|
||||||
|
componentRequirementSelections[requirementId] || []
|
||||||
|
|
||||||
|
const getPieceRequirementEntries = (requirementId: string): AnyRecord[] =>
|
||||||
|
pieceRequirementSelections[requirementId] || []
|
||||||
|
|
||||||
|
const getProductRequirementEntries = (requirementId: string): AnyRecord[] =>
|
||||||
|
productRequirementSelections[requirementId] || []
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Entry factories
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const createComponentSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => ({
|
||||||
|
typeComposantId: requirement?.typeComposantId || (requirement?.typeComposant as AnyRecord)?.id || null,
|
||||||
|
composantId: source?.composantId || null,
|
||||||
|
definition: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const createPieceSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => ({
|
||||||
|
typePieceId: requirement?.typePieceId || (requirement?.typePiece as AnyRecord)?.id || null,
|
||||||
|
pieceId: source?.pieceId || null,
|
||||||
|
definition: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const createProductSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => ({
|
||||||
|
typeProductId:
|
||||||
|
source?.typeProductId ||
|
||||||
|
requirement?.typeProductId ||
|
||||||
|
(requirement?.typeProduct as AnyRecord)?.id ||
|
||||||
|
null,
|
||||||
|
productId: source?.productId || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Selected piece IDs (for dedup)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const selectedPieceIds = computed(() => {
|
||||||
|
const ids: string[] = []
|
||||||
|
Object.values(pieceRequirementSelections).forEach((entries) => {
|
||||||
|
;(entries || []).forEach((entry) => {
|
||||||
|
if (entry?.pieceId) ids.push(entry.pieceId as string)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return ids
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CRUD operations
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const addComponentSelectionEntry = (requirement: AnyRecord): void => {
|
||||||
|
const entries = getComponentRequirementEntries(requirement.id as string)
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
if (max !== null && entries.length >= max) {
|
||||||
|
toast.showError(
|
||||||
|
`Vous ne pouvez pas ajouter plus de ${max} composant(s) pour ${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'ce groupe'}`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
componentRequirementSelections[requirement.id as string] = [
|
||||||
|
...entries,
|
||||||
|
createComponentSelectionEntry(requirement),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeComponentSelectionEntry = (requirementId: string, index: number): void => {
|
||||||
|
const entries = getComponentRequirementEntries(requirementId)
|
||||||
|
componentRequirementSelections[requirementId] = entries.filter((_: unknown, i: number) => i !== index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPieceSelectionEntry = (requirement: AnyRecord): void => {
|
||||||
|
const entries = getPieceRequirementEntries(requirement.id as string)
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
if (max !== null && entries.length >= max) {
|
||||||
|
toast.showError(
|
||||||
|
`Vous ne pouvez pas ajouter plus de ${max} pièce(s) pour ${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'ce groupe'}`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pieceRequirementSelections[requirement.id as string] = [
|
||||||
|
...entries,
|
||||||
|
createPieceSelectionEntry(requirement),
|
||||||
|
]
|
||||||
|
fetchPieceOptions(requirement, entries.length).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePieceSelectionEntry = (requirementId: string, index: number): void => {
|
||||||
|
const entries = getPieceRequirementEntries(requirementId)
|
||||||
|
pieceRequirementSelections[requirementId] = entries.filter((_: unknown, i: number) => i !== index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addProductSelectionEntry = (requirement: AnyRecord): void => {
|
||||||
|
const entries = getProductRequirementEntries(requirement.id as string)
|
||||||
|
const max = (requirement.maxCount ?? null) as number | null
|
||||||
|
if (max !== null && entries.length >= max) {
|
||||||
|
toast.showError(
|
||||||
|
`Vous ne pouvez pas ajouter plus de ${max} produit(s) pour ${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'ce groupe'}`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
productRequirementSelections[requirement.id as string] = [
|
||||||
|
...entries,
|
||||||
|
createProductSelectionEntry(requirement),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeProductSelectionEntry = (requirementId: string, index: number): void => {
|
||||||
|
const entries = getProductRequirementEntries(requirementId)
|
||||||
|
productRequirementSelections[requirementId] = entries.filter((_: unknown, i: number) => i !== index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Selection setters
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const setComponentRequirementComponent = (
|
||||||
|
requirement: AnyRecord,
|
||||||
|
index: number,
|
||||||
|
componentId: string,
|
||||||
|
): void => {
|
||||||
|
const entries = getComponentRequirementEntries(requirement.id as string)
|
||||||
|
const entry = entries[index]
|
||||||
|
if (!entry) return
|
||||||
|
entry.composantId = componentId || null
|
||||||
|
if (componentId) {
|
||||||
|
const component = findComponentById(componentId)
|
||||||
|
entry.typeComposantId = component?.typeComposantId || requirement?.typeComposantId || null
|
||||||
|
} else {
|
||||||
|
entry.typeComposantId = requirement?.typeComposantId || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPieceRequirementPiece = (
|
||||||
|
requirement: AnyRecord,
|
||||||
|
index: number,
|
||||||
|
pieceId: string,
|
||||||
|
): void => {
|
||||||
|
const entries = getPieceRequirementEntries(requirement.id as string)
|
||||||
|
const entry = entries[index]
|
||||||
|
if (!entry) return
|
||||||
|
entry.pieceId = pieceId || null
|
||||||
|
if (pieceId) {
|
||||||
|
const piece = findPieceById(pieceId) || findPieceInCachedOptions(pieceId)
|
||||||
|
entry.typePieceId = piece?.typePieceId || requirement?.typePieceId || null
|
||||||
|
if (piece) cachePieceIfMissing(piece as AnyRecord)
|
||||||
|
} else {
|
||||||
|
entry.typePieceId = requirement?.typePieceId || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setProductRequirementProduct = (
|
||||||
|
requirement: AnyRecord,
|
||||||
|
index: number,
|
||||||
|
productId: string,
|
||||||
|
findProductById: (id: string) => AnyRecord | null,
|
||||||
|
): void => {
|
||||||
|
const entries = getProductRequirementEntries(requirement.id as string)
|
||||||
|
const entry = entries[index]
|
||||||
|
if (!entry) return
|
||||||
|
|
||||||
|
const normalizedProductId = productId || null
|
||||||
|
entry.productId = normalizedProductId
|
||||||
|
|
||||||
|
if (normalizedProductId) {
|
||||||
|
const product = findProductById(normalizedProductId)
|
||||||
|
entry.typeProductId =
|
||||||
|
product?.typeProductId ||
|
||||||
|
(product?.typeProduct as AnyRecord)?.id ||
|
||||||
|
entry.typeProductId ||
|
||||||
|
requirement?.typeProductId ||
|
||||||
|
(requirement?.typeProduct as AnyRecord)?.id ||
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
entry.typeProductId =
|
||||||
|
requirement?.typeProductId ||
|
||||||
|
(requirement?.typeProduct as AnyRecord)?.id ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Bulk operations
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const clearRequirementSelections = (): void => {
|
||||||
|
Object.keys(componentRequirementSelections).forEach((key) => {
|
||||||
|
delete componentRequirementSelections[key]
|
||||||
|
})
|
||||||
|
Object.keys(pieceRequirementSelections).forEach((key) => {
|
||||||
|
delete pieceRequirementSelections[key]
|
||||||
|
})
|
||||||
|
Object.keys(productRequirementSelections).forEach((key) => {
|
||||||
|
delete productRequirementSelections[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeRequirementSelections = (type: AnyRecord): void => {
|
||||||
|
const componentRequirements = (type.componentRequirements || []) as AnyRecord[]
|
||||||
|
const pieceRequirements = (type.pieceRequirements || []) as AnyRecord[]
|
||||||
|
const productRequirements = (type.productRequirements || []) as AnyRecord[]
|
||||||
|
|
||||||
|
componentRequirements.forEach((requirement) => {
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const initialCount = Math.max(min, requirement.required ? 1 : 0)
|
||||||
|
if (initialCount > 0) {
|
||||||
|
componentRequirementSelections[requirement.id as string] = Array.from(
|
||||||
|
{ length: initialCount },
|
||||||
|
() => createComponentSelectionEntry(requirement),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
componentRequirementSelections[requirement.id as string] = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pieceRequirements.forEach((requirement) => {
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const initialCount = Math.max(min, requirement.required ? 1 : 0)
|
||||||
|
if (initialCount > 0) {
|
||||||
|
pieceRequirementSelections[requirement.id as string] = Array.from(
|
||||||
|
{ length: initialCount },
|
||||||
|
() => createPieceSelectionEntry(requirement),
|
||||||
|
)
|
||||||
|
pieceRequirementSelections[requirement.id as string].forEach((_: unknown, index: number) => {
|
||||||
|
fetchPieceOptions(requirement, index).catch(() => {})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
pieceRequirementSelections[requirement.id as string] = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
productRequirements.forEach((requirement) => {
|
||||||
|
const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
|
||||||
|
const initialCount = Math.max(min, requirement.required ? 1 : 0)
|
||||||
|
if (initialCount > 0) {
|
||||||
|
productRequirementSelections[requirement.id as string] = Array.from(
|
||||||
|
{ length: initialCount },
|
||||||
|
() => createProductSelectionEntry(requirement),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
productRequirementSelections[requirement.id as string] = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
componentRequirementSelections,
|
||||||
|
pieceRequirementSelections,
|
||||||
|
productRequirementSelections,
|
||||||
|
pieceOptionsByKey,
|
||||||
|
pieceLoadingByKey,
|
||||||
|
selectedPieceIds,
|
||||||
|
getPieceKey,
|
||||||
|
findPieceInCachedOptions,
|
||||||
|
fetchPieceOptions,
|
||||||
|
getComponentRequirementEntries,
|
||||||
|
getPieceRequirementEntries,
|
||||||
|
getProductRequirementEntries,
|
||||||
|
addComponentSelectionEntry,
|
||||||
|
removeComponentSelectionEntry,
|
||||||
|
addPieceSelectionEntry,
|
||||||
|
removePieceSelectionEntry,
|
||||||
|
addProductSelectionEntry,
|
||||||
|
removeProductSelectionEntry,
|
||||||
|
setComponentRequirementComponent,
|
||||||
|
setPieceRequirementPiece,
|
||||||
|
setProductRequirementProduct,
|
||||||
|
clearRequirementSelections,
|
||||||
|
initializeRequirementSelections,
|
||||||
|
}
|
||||||
|
}
|
||||||
298
app/composables/useMachineHierarchy.ts
Normal file
298
app/composables/useMachineHierarchy.ts
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* Builds a component/piece hierarchy tree from flat machine link arrays.
|
||||||
|
*
|
||||||
|
* Extracted from pages/machine/[id].vue to keep the page orchestrator lean.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { resolveIdentifier, resolveProductReference, getProductDisplay } from '~/shared/utils/productDisplayUtils'
|
||||||
|
import { resolveConstructeurs, uniqueConstructeurIds, type ConstructeurSummary } from '~/shared/constructeurUtils'
|
||||||
|
|
||||||
|
type AnyRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const collectConstructeurs = (
|
||||||
|
allConstructeurs: AnyRecord[],
|
||||||
|
...sources: unknown[]
|
||||||
|
): AnyRecord[] => {
|
||||||
|
const ids = uniqueConstructeurIds(...sources)
|
||||||
|
if (!ids.length) return []
|
||||||
|
|
||||||
|
const pools = sources
|
||||||
|
.flatMap((source) => {
|
||||||
|
if (Array.isArray(source)) return [source]
|
||||||
|
if (source && typeof source === 'object' && (source as AnyRecord).id) return [[source]]
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
.filter(Boolean) as AnyRecord[][]
|
||||||
|
|
||||||
|
// ConstructeurSummary and AnyRecord are structurally compatible at runtime
|
||||||
|
const allPools = [...pools, allConstructeurs] as unknown as Array<ConstructeurSummary[]>
|
||||||
|
return resolveConstructeurs(ids, ...allPools) as unknown as AnyRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Link array resolution
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const resolveLinkArray = (source: unknown, keys: string[]): unknown[] | null => {
|
||||||
|
if (!source || typeof source !== 'object') return null
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = (source as AnyRecord)[key]
|
||||||
|
if (Array.isArray(value)) return value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Merge trees (for incremental updates)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function mergePieceLists(existing: AnyRecord[] = [], updates: AnyRecord[] = []): AnyRecord[] {
|
||||||
|
if (!existing.length) {
|
||||||
|
return updates.map((piece) => ({ ...piece, constructeurs: piece.constructeurs || [] }))
|
||||||
|
}
|
||||||
|
if (!updates.length) {
|
||||||
|
return existing.map((piece) => ({ ...piece, constructeurs: piece.constructeurs || [] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMap = new Map<unknown, AnyRecord>(
|
||||||
|
updates.map((piece) => [piece.id, { ...piece, constructeurs: piece.constructeurs || [] }]),
|
||||||
|
)
|
||||||
|
const merged = existing.map((piece) => {
|
||||||
|
const update = updateMap.get(piece.id)
|
||||||
|
if (!update) return piece
|
||||||
|
return { ...piece, ...update, customFields: update.customFields ?? piece.customFields }
|
||||||
|
})
|
||||||
|
|
||||||
|
updates.forEach((update) => {
|
||||||
|
if (!existing.some((piece) => piece.id === update.id)) merged.push(update)
|
||||||
|
})
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeComponentTrees(existing: AnyRecord[] = [], updates: AnyRecord[] = []): AnyRecord[] {
|
||||||
|
if (!existing.length) {
|
||||||
|
return updates.map((component) => ({
|
||||||
|
...component,
|
||||||
|
constructeurs: component.constructeurs || [],
|
||||||
|
pieces: ((component.pieces || []) as AnyRecord[]).map((piece) => ({
|
||||||
|
...piece,
|
||||||
|
constructeurs: piece.constructeurs || [],
|
||||||
|
})),
|
||||||
|
subComponents: mergeComponentTrees([], (component.subComponents || []) as AnyRecord[]),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (!updates.length) return existing
|
||||||
|
|
||||||
|
const updateMap = new Map<unknown, AnyRecord>(
|
||||||
|
updates.map((component) => [
|
||||||
|
component.id,
|
||||||
|
{
|
||||||
|
...component,
|
||||||
|
constructeurs: component.constructeurs || [],
|
||||||
|
pieces: ((component.pieces || []) as AnyRecord[]).map((piece) => ({
|
||||||
|
...piece,
|
||||||
|
constructeurs: piece.constructeurs || [],
|
||||||
|
})),
|
||||||
|
subComponents: mergeComponentTrees([], (component.subComponents || []) as AnyRecord[]),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
const merged: AnyRecord[] = existing.map((component) => {
|
||||||
|
const update = updateMap.get(component.id)
|
||||||
|
if (!update) {
|
||||||
|
return {
|
||||||
|
...component,
|
||||||
|
constructeurs: component.constructeurs || [],
|
||||||
|
pieces: mergePieceLists((component.pieces || []) as AnyRecord[], []),
|
||||||
|
subComponents: mergeComponentTrees((component.subComponents || []) as AnyRecord[], []),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...component,
|
||||||
|
...update,
|
||||||
|
customFields: update.customFields ?? component.customFields,
|
||||||
|
pieces: mergePieceLists((component.pieces || []) as AnyRecord[], (update.pieces || []) as AnyRecord[]),
|
||||||
|
subComponents: mergeComponentTrees(
|
||||||
|
(component.subComponents || []) as AnyRecord[],
|
||||||
|
(update.subComponents || []) as AnyRecord[],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
updates.forEach((update) => {
|
||||||
|
if (!existing.some((component) => component.id === update.id)) merged.push(update)
|
||||||
|
})
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Build hierarchy from links
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const buildMachineHierarchyFromLinks = (
|
||||||
|
componentLinks: AnyRecord[] = [],
|
||||||
|
pieceLinks: AnyRecord[] = [],
|
||||||
|
findProductById: (id: string) => AnyRecord | null,
|
||||||
|
allConstructeurs: AnyRecord[] = [],
|
||||||
|
): { components: AnyRecord[]; machinePieces: AnyRecord[] } => {
|
||||||
|
const normalizeComponentLinkId = (link: AnyRecord) =>
|
||||||
|
resolveIdentifier(link?.id, link?.linkId, link?.machineComponentLinkId)
|
||||||
|
|
||||||
|
const normalizePieceLinkId = (link: AnyRecord) =>
|
||||||
|
resolveIdentifier(link?.id, link?.linkId, link?.machinePieceLinkId)
|
||||||
|
|
||||||
|
const createPieceNode = (link: AnyRecord, parentComponentName: string | null = null): AnyRecord | null => {
|
||||||
|
if (!link || typeof link !== 'object') return null
|
||||||
|
|
||||||
|
const appliedPiece = (link.piece && typeof link.piece === 'object' ? link.piece : {}) as AnyRecord
|
||||||
|
const originalPiece = (link.originalPiece && typeof link.originalPiece === 'object' ? link.originalPiece : null) as AnyRecord | null
|
||||||
|
|
||||||
|
const requirement = (link.typeMachinePieceRequirement || appliedPiece.typeMachinePieceRequirement || originalPiece?.typeMachinePieceRequirement || null) as AnyRecord | null
|
||||||
|
|
||||||
|
const machinePieceLinkId = normalizePieceLinkId(link)
|
||||||
|
const pieceId = resolveIdentifier(appliedPiece.id, appliedPiece.pieceId, link.pieceId)
|
||||||
|
|
||||||
|
const overrides = (link.overrides || null) as AnyRecord | null
|
||||||
|
|
||||||
|
const basePiece: AnyRecord = {
|
||||||
|
...appliedPiece,
|
||||||
|
id: appliedPiece.id || pieceId || machinePieceLinkId || `piece-${machinePieceLinkId}`,
|
||||||
|
pieceId,
|
||||||
|
name: overrides?.name || appliedPiece.name || (appliedPiece.definition as AnyRecord)?.name || (appliedPiece.definition as AnyRecord)?.role || originalPiece?.name || 'Pièce',
|
||||||
|
reference: overrides?.reference || appliedPiece.reference || (appliedPiece.definition as AnyRecord)?.reference || originalPiece?.reference || null,
|
||||||
|
prix: overrides?.prix ?? appliedPiece.prix ?? originalPiece?.prix ?? null,
|
||||||
|
constructeur: appliedPiece.constructeur || originalPiece?.constructeur || null,
|
||||||
|
constructeurId: appliedPiece.constructeurId || (appliedPiece.constructeur as AnyRecord)?.id || originalPiece?.constructeurId || null,
|
||||||
|
documents: Array.isArray(appliedPiece.documents) ? appliedPiece.documents : Array.isArray(originalPiece?.documents) ? originalPiece!.documents : [],
|
||||||
|
typePiece: appliedPiece.typePiece || requirement?.typePiece || null,
|
||||||
|
typePieceId: appliedPiece.typePieceId || (appliedPiece.typePiece as AnyRecord)?.id || requirement?.typePieceId || (requirement?.typePiece as AnyRecord)?.id || null,
|
||||||
|
typeMachinePieceRequirement: requirement,
|
||||||
|
typeMachinePieceRequirementId: requirement?.id || null,
|
||||||
|
requirementId: requirement?.id || null,
|
||||||
|
overrides,
|
||||||
|
originalPiece,
|
||||||
|
machinePieceLink: link,
|
||||||
|
machinePieceLinkId,
|
||||||
|
linkId: machinePieceLinkId,
|
||||||
|
parentComponentLinkId: resolveIdentifier(link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, appliedPiece.parentComponentLinkId),
|
||||||
|
parentComponentId: resolveIdentifier(appliedPiece.parentComponentId, link.parentComponentId),
|
||||||
|
parentComponentName,
|
||||||
|
parentLinkId: resolveIdentifier(link.parentLinkId, link.parentMachinePieceLinkId, appliedPiece.parentLinkId),
|
||||||
|
parentPieceLinkId: resolveIdentifier(link.parentPieceLinkId, appliedPiece.parentPieceLinkId),
|
||||||
|
parentPieceId: resolveIdentifier(appliedPiece.parentPieceId, link.parentPieceId),
|
||||||
|
parentMachineComponentRequirementId: resolveIdentifier(appliedPiece.parentMachineComponentRequirementId, link.parentMachineComponentRequirementId),
|
||||||
|
parentMachinePieceRequirementId: resolveIdentifier(appliedPiece.parentMachinePieceRequirementId, link.parentMachinePieceRequirementId),
|
||||||
|
definition: appliedPiece.definition || originalPiece?.definition || {},
|
||||||
|
customFields: appliedPiece.customFields || [],
|
||||||
|
skeletonOnly: !pieceId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedProductId = resolveIdentifier(appliedPiece.productId, (appliedPiece.product as AnyRecord)?.id, link.productId, (link.product as AnyRecord)?.id, originalPiece?.productId, (originalPiece?.product as AnyRecord)?.id)
|
||||||
|
const resolvedProduct = (appliedPiece.product || link.product || originalPiece?.product || (resolvedProductId ? findProductById(resolvedProductId) : null) || null) as AnyRecord | null
|
||||||
|
|
||||||
|
const constructeurs = collectConstructeurs(allConstructeurs, appliedPiece.constructeurs, appliedPiece.constructeur, appliedPiece.constructeurIds, appliedPiece.constructeurId, originalPiece?.constructeurs, originalPiece?.constructeur, originalPiece?.constructeurIds, originalPiece?.constructeurId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...basePiece,
|
||||||
|
constructeurs,
|
||||||
|
constructeur: constructeurs[0] || basePiece.constructeur || null,
|
||||||
|
constructeurId: (constructeurs[0] as AnyRecord)?.id || basePiece.constructeurId || null,
|
||||||
|
productId: resolvedProductId || appliedPiece.productId || null,
|
||||||
|
product: resolvedProduct || appliedPiece.product || null,
|
||||||
|
__productDisplay: getProductDisplay({ product: resolvedProduct || appliedPiece.product || null, productId: resolvedProductId || appliedPiece.productId || null } as AnyRecord, findProductById),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createComponentNode = (link: AnyRecord): AnyRecord | null => {
|
||||||
|
if (!link || typeof link !== 'object') return null
|
||||||
|
|
||||||
|
const appliedComponent = (link.composant && typeof link.composant === 'object' ? link.composant : {}) as AnyRecord
|
||||||
|
const originalComponent = (link.originalComposant && typeof link.originalComposant === 'object' ? link.originalComposant : null) as AnyRecord | null
|
||||||
|
|
||||||
|
const requirement = (link.typeMachineComponentRequirement || appliedComponent.typeMachineComponentRequirement || originalComponent?.typeMachineComponentRequirement || null) as AnyRecord | null
|
||||||
|
|
||||||
|
const machineComponentLinkId = normalizeComponentLinkId(link)
|
||||||
|
const composantId = resolveIdentifier(appliedComponent.id, appliedComponent.composantId, link.composantId)
|
||||||
|
|
||||||
|
const compOverrides = (link.overrides || null) as AnyRecord | null
|
||||||
|
|
||||||
|
const componentName = (compOverrides?.name || appliedComponent.name || (appliedComponent.definition as AnyRecord)?.alias || (appliedComponent.definition as AnyRecord)?.name || originalComponent?.name || 'Composant') as string
|
||||||
|
|
||||||
|
const pieces = Array.isArray(link.pieceLinks)
|
||||||
|
? (link.pieceLinks as AnyRecord[]).map((pl) => createPieceNode(pl, componentName)).filter(Boolean) as AnyRecord[]
|
||||||
|
: []
|
||||||
|
|
||||||
|
const subComponents = Array.isArray(link.childLinks)
|
||||||
|
? (link.childLinks as AnyRecord[]).map(createComponentNode).filter(Boolean) as AnyRecord[]
|
||||||
|
: []
|
||||||
|
|
||||||
|
const resolvedProductId = resolveIdentifier(appliedComponent.productId, (appliedComponent.product as AnyRecord)?.id, link.productId, (link.product as AnyRecord)?.id, originalComponent?.productId, (originalComponent?.product as AnyRecord)?.id)
|
||||||
|
const resolvedProduct = (appliedComponent.product || link.product || originalComponent?.product || (resolvedProductId ? findProductById(resolvedProductId) : null) || null) as AnyRecord | null
|
||||||
|
|
||||||
|
const baseComponent: AnyRecord = {
|
||||||
|
...appliedComponent,
|
||||||
|
id: appliedComponent.id || composantId || machineComponentLinkId || `component-${machineComponentLinkId}`,
|
||||||
|
composantId,
|
||||||
|
name: componentName,
|
||||||
|
reference: compOverrides?.reference || appliedComponent.reference || (appliedComponent.definition as AnyRecord)?.reference || originalComponent?.reference || null,
|
||||||
|
prix: compOverrides?.prix ?? appliedComponent.prix ?? originalComponent?.prix ?? null,
|
||||||
|
constructeur: appliedComponent.constructeur || originalComponent?.constructeur || null,
|
||||||
|
constructeurId: appliedComponent.constructeurId || (appliedComponent.constructeur as AnyRecord)?.id || originalComponent?.constructeurId || null,
|
||||||
|
documents: Array.isArray(appliedComponent.documents) ? appliedComponent.documents : Array.isArray(originalComponent?.documents) ? originalComponent!.documents : [],
|
||||||
|
typeComposant: appliedComponent.typeComposant || requirement?.typeComposant || null,
|
||||||
|
typeComposantId: appliedComponent.typeComposantId || (appliedComponent.typeComposant as AnyRecord)?.id || requirement?.typeComposantId || (requirement?.typeComposant as AnyRecord)?.id || null,
|
||||||
|
typeMachineComponentRequirement: requirement,
|
||||||
|
typeMachineComponentRequirementId: requirement?.id || null,
|
||||||
|
requirementId: requirement?.id || null,
|
||||||
|
overrides: compOverrides,
|
||||||
|
machineComponentLinkOverrides: compOverrides,
|
||||||
|
definitionOverrides: compOverrides,
|
||||||
|
originalComposant: originalComponent,
|
||||||
|
machineComponentLink: link,
|
||||||
|
machineComponentLinkId,
|
||||||
|
componentLinkId: machineComponentLinkId,
|
||||||
|
parentComponentLinkId: resolveIdentifier(link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, appliedComponent.parentComponentLinkId),
|
||||||
|
parentComposantId: resolveIdentifier(appliedComponent.parentComposantId, link.parentComponentId),
|
||||||
|
parentRequirementId: resolveIdentifier(appliedComponent.parentRequirementId, link.parentRequirementId),
|
||||||
|
parentMachineComponentRequirementId: resolveIdentifier(appliedComponent.parentMachineComponentRequirementId, link.parentMachineComponentRequirementId),
|
||||||
|
parentMachinePieceRequirementId: resolveIdentifier(appliedComponent.parentMachinePieceRequirementId, link.parentMachinePieceRequirementId),
|
||||||
|
definition: appliedComponent.definition || originalComponent?.definition || {},
|
||||||
|
customFields: appliedComponent.customFields || [],
|
||||||
|
pieces,
|
||||||
|
subComponents,
|
||||||
|
subcomponents: subComponents,
|
||||||
|
sousComposants: subComponents,
|
||||||
|
skeletonOnly: !composantId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const constructeurs = collectConstructeurs(allConstructeurs, appliedComponent.constructeurs, appliedComponent.constructeur, appliedComponent.constructeurIds, appliedComponent.constructeurId, originalComponent?.constructeurs, originalComponent?.constructeur, originalComponent?.constructeurIds, originalComponent?.constructeurId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseComponent,
|
||||||
|
constructeurs,
|
||||||
|
constructeur: constructeurs[0] || baseComponent.constructeur || null,
|
||||||
|
constructeurId: (constructeurs[0] as AnyRecord)?.id || baseComponent.constructeurId || null,
|
||||||
|
productId: resolvedProductId || appliedComponent.productId || null,
|
||||||
|
product: resolvedProduct || appliedComponent.product || null,
|
||||||
|
__productDisplay: getProductDisplay({ product: resolvedProduct || appliedComponent.product || null, productId: resolvedProductId || appliedComponent.productId || null } as AnyRecord, findProductById),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootComponents = (Array.isArray(componentLinks) ? componentLinks : [])
|
||||||
|
.filter((link) => !resolveIdentifier(link?.parentComponentLinkId, link?.parentLinkId, link?.parentMachineComponentLinkId))
|
||||||
|
.map(createComponentNode)
|
||||||
|
.filter(Boolean) as AnyRecord[]
|
||||||
|
|
||||||
|
const machinePieces = (Array.isArray(pieceLinks) ? pieceLinks : [])
|
||||||
|
.filter((link) => !resolveIdentifier(link?.parentComponentLinkId, link?.parentLinkId, link?.parentMachineComponentLinkId))
|
||||||
|
.map((link) => createPieceNode(link, null))
|
||||||
|
.filter(Boolean) as AnyRecord[]
|
||||||
|
|
||||||
|
return { components: rootComponents, machinePieces }
|
||||||
|
}
|
||||||
164
app/composables/useMachinePrint.ts
Normal file
164
app/composables/useMachinePrint.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Machine print selection and execution logic.
|
||||||
|
*
|
||||||
|
* Extracted from pages/machine/[id].vue.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, reactive, nextTick } from 'vue'
|
||||||
|
import { buildMachinePrintContext, buildMachinePrintHtml } from '~/utils/printTemplates/machineReport'
|
||||||
|
import { resolveIdentifier } from '~/shared/utils/productDisplayUtils'
|
||||||
|
|
||||||
|
type AnyRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
export interface PrintSelection {
|
||||||
|
machine: { info: boolean; customFields: boolean; documents: boolean }
|
||||||
|
components: Record<string, boolean>
|
||||||
|
pieces: Record<string, boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMachinePrint() {
|
||||||
|
const printModalOpen = ref(false)
|
||||||
|
const printSelection = reactive<PrintSelection>({
|
||||||
|
machine: { info: true, customFields: true, documents: true },
|
||||||
|
components: {},
|
||||||
|
pieces: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ensurePrintSelectionEntries = (
|
||||||
|
components: AnyRecord[],
|
||||||
|
machinePieces: AnyRecord[],
|
||||||
|
) => {
|
||||||
|
printSelection.machine.info ??= true
|
||||||
|
printSelection.machine.customFields ??= true
|
||||||
|
printSelection.machine.documents ??= true
|
||||||
|
|
||||||
|
const ensureComponent = (component: AnyRecord) => {
|
||||||
|
if (component?.id !== undefined && printSelection.components[component.id as string] === undefined) {
|
||||||
|
printSelection.components[component.id as string] = true
|
||||||
|
}
|
||||||
|
;((component.pieces || []) as AnyRecord[]).forEach((piece) => {
|
||||||
|
if (piece?.id !== undefined && printSelection.pieces[piece.id as string] === undefined) {
|
||||||
|
printSelection.pieces[piece.id as string] = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
;((component.subComponents || []) as AnyRecord[]).forEach(ensureComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
components.forEach(ensureComponent)
|
||||||
|
machinePieces.forEach((piece) => {
|
||||||
|
if (piece?.id !== undefined && printSelection.pieces[piece.id as string] === undefined) {
|
||||||
|
printSelection.pieces[piece.id as string] = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAllPrintSelection = (
|
||||||
|
value: boolean,
|
||||||
|
components: AnyRecord[],
|
||||||
|
machinePieces: AnyRecord[],
|
||||||
|
) => {
|
||||||
|
ensurePrintSelectionEntries(components, machinePieces)
|
||||||
|
printSelection.machine.info = value
|
||||||
|
printSelection.machine.customFields = value
|
||||||
|
printSelection.machine.documents = value
|
||||||
|
Object.keys(printSelection.components).forEach((key) => {
|
||||||
|
printSelection.components[key] = value
|
||||||
|
})
|
||||||
|
Object.keys(printSelection.pieces).forEach((key) => {
|
||||||
|
printSelection.pieces[key] = value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPrintModal = (components: AnyRecord[], machinePieces: AnyRecord[]) => {
|
||||||
|
ensurePrintSelectionEntries(components, machinePieces)
|
||||||
|
printModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePrintModal = () => {
|
||||||
|
printModalOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const printMachine = (
|
||||||
|
machine: AnyRecord,
|
||||||
|
machineName: string,
|
||||||
|
machineReference: string,
|
||||||
|
machinePieces: AnyRecord[],
|
||||||
|
components: AnyRecord[],
|
||||||
|
currentSelection: PrintSelection = printSelection,
|
||||||
|
) => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
// machineReport.js has no type annotations; cast to avoid inferred never[] params
|
||||||
|
const context = (buildMachinePrintContext as unknown as (config: Record<string, unknown>) => Record<string, unknown>)({
|
||||||
|
machine,
|
||||||
|
machineName,
|
||||||
|
machineReference,
|
||||||
|
machinePieces,
|
||||||
|
components,
|
||||||
|
selection: currentSelection,
|
||||||
|
})
|
||||||
|
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'))
|
||||||
|
.map((node) => node.outerHTML)
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const htmlContent = buildMachinePrintHtml(context, styles)
|
||||||
|
|
||||||
|
const iframe = document.createElement('iframe')
|
||||||
|
iframe.style.position = 'fixed'
|
||||||
|
iframe.style.right = '0'
|
||||||
|
iframe.style.bottom = '0'
|
||||||
|
iframe.style.width = '0'
|
||||||
|
iframe.style.height = '0'
|
||||||
|
iframe.style.border = '0'
|
||||||
|
iframe.setAttribute('title', 'print-frame')
|
||||||
|
document.body.appendChild(iframe)
|
||||||
|
|
||||||
|
const iframeWindow = iframe.contentWindow
|
||||||
|
const iframeDocument = iframe.contentDocument || iframeWindow?.document
|
||||||
|
if (!iframeDocument || !iframeWindow) {
|
||||||
|
iframe.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iframeDocument.open()
|
||||||
|
iframeDocument.write(htmlContent)
|
||||||
|
iframeDocument.close()
|
||||||
|
|
||||||
|
const triggerPrint = () => {
|
||||||
|
iframeWindow.focus()
|
||||||
|
iframeWindow.print()
|
||||||
|
setTimeout(() => {
|
||||||
|
iframe.remove()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iframeDocument.readyState === 'complete') {
|
||||||
|
setTimeout(triggerPrint, 50)
|
||||||
|
} else {
|
||||||
|
iframe.onload = () => setTimeout(triggerPrint, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrintConfirm = async (
|
||||||
|
machine: AnyRecord,
|
||||||
|
machineName: string,
|
||||||
|
machineReference: string,
|
||||||
|
machinePieces: AnyRecord[],
|
||||||
|
components: AnyRecord[],
|
||||||
|
) => {
|
||||||
|
closePrintModal()
|
||||||
|
await nextTick()
|
||||||
|
printMachine(machine, machineName, machineReference, machinePieces, components, printSelection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
printModalOpen,
|
||||||
|
printSelection,
|
||||||
|
ensurePrintSelectionEntries,
|
||||||
|
setAllPrintSelection,
|
||||||
|
openPrintModal,
|
||||||
|
closePrintModal,
|
||||||
|
printMachine,
|
||||||
|
handlePrintConfirm,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,34 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
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)
|
const loadingPieceTypes = ref(false)
|
||||||
|
|
||||||
export function usePieceTypes () {
|
export function usePieceTypes() {
|
||||||
const { showSuccess, showError } = useToast()
|
const { showSuccess, showError } = useToast()
|
||||||
|
|
||||||
const generateCodeFromName = (name) => {
|
const generateCodeFromName = (name: string): string => {
|
||||||
return (name || '')
|
return (name || '')
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/[\u0300-\u036F]/g, '')
|
.replace(/[\u0300-\u036F]/g, '')
|
||||||
@@ -18,24 +38,26 @@ export function usePieceTypes () {
|
|||||||
.replace(/-+/g, '-') || 'type'
|
.replace(/-+/g, '-') || 'type'
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadPieceTypes = async () => {
|
const loadPieceTypes = async (): Promise<PieceTypeResult> => {
|
||||||
loadingPieceTypes.value = true
|
loadingPieceTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await listModelTypes({
|
const data = await listModelTypes({
|
||||||
category: 'PIECE',
|
category: 'PIECE',
|
||||||
sort: 'name',
|
sort: 'name',
|
||||||
dir: 'asc',
|
dir: 'asc',
|
||||||
limit: 200
|
limit: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
pieceTypes.value = data.items.map(item => ({
|
pieceTypes.value = data.items.map((item) => ({
|
||||||
...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 }
|
return { success: true, data: pieceTypes.value }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Impossible de charger les types de pièce: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -43,21 +65,22 @@ export function usePieceTypes () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPieceType = async (payload) => {
|
const createPieceType = async (payload: PieceTypePayload): Promise<PieceTypeResult> => {
|
||||||
loadingPieceTypes.value = true
|
loadingPieceTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await createModelType({
|
const data = await createModelType({
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
code: payload.code || generateCodeFromName(payload.name),
|
code: payload.code || generateCodeFromName(payload.name),
|
||||||
category: 'PIECE',
|
category: 'PIECE',
|
||||||
notes: payload.description ?? payload.notes,
|
notes: payload.description ?? payload.notes ?? undefined,
|
||||||
description: payload.description ?? null,
|
description: payload.description ?? undefined,
|
||||||
structure: payload.structure
|
structure: payload.structure ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalized = {
|
const normalized: PieceType = {
|
||||||
...data,
|
...data,
|
||||||
description: data.description ?? data.notes ?? null
|
structure: data.structure as PieceModelStructure | null,
|
||||||
|
description: data.description ?? data.notes ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
pieceTypes.value.push(normalized)
|
pieceTypes.value.push(normalized)
|
||||||
@@ -65,7 +88,8 @@ export function usePieceTypes () {
|
|||||||
|
|
||||||
return { success: true, data: normalized }
|
return { success: true, data: normalized }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la création du type de pièce: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} 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
|
loadingPieceTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await updateModelType(id, {
|
const data = await updateModelType(id, {
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
description: payload.description,
|
description: payload.description ?? undefined,
|
||||||
notes: payload.notes,
|
notes: payload.notes ?? undefined,
|
||||||
code: payload.code,
|
code: payload.code,
|
||||||
structure: payload.structure
|
structure: payload.structure ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalized = {
|
const normalized: PieceType = {
|
||||||
...data,
|
...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) {
|
if (index !== -1) {
|
||||||
pieceTypes.value[index] = normalized
|
pieceTypes.value[index] = normalized
|
||||||
}
|
}
|
||||||
showSuccess(`Type de pièce "${data.name}" mis à jour`)
|
showSuccess(`Type de pièce "${data.name}" mis à jour`)
|
||||||
|
|
||||||
return {
|
return { success: true, data: normalized }
|
||||||
success: true,
|
|
||||||
data: normalized
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la mise à jour du type de pièce: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -108,15 +131,16 @@ export function usePieceTypes () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletePieceType = async (id) => {
|
const deletePieceType = async (id: string): Promise<PieceTypeResult> => {
|
||||||
loadingPieceTypes.value = true
|
loadingPieceTypes.value = true
|
||||||
try {
|
try {
|
||||||
await deleteModelType(id)
|
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é')
|
showSuccess('Type de pièce supprimé')
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la suppression du type de pièce: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -135,6 +159,6 @@ export function usePieceTypes () {
|
|||||||
updatePieceType,
|
updatePieceType,
|
||||||
deletePieceType,
|
deletePieceType,
|
||||||
getPieceTypes,
|
getPieceTypes,
|
||||||
isPieceTypeLoading
|
isPieceTypeLoading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,45 +2,84 @@ import { ref } from 'vue'
|
|||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs } from './useConstructeurs'
|
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
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 total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const extractCollection = (payload) => {
|
const extractCollection = (payload: unknown): Piece[] => {
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) {
|
||||||
return payload
|
return payload as Piece[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.member)) {
|
const p = payload as Record<string, unknown> | null
|
||||||
return payload.member
|
if (Array.isArray(p?.member)) {
|
||||||
|
return p.member as Piece[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.['hydra:member'])) {
|
if (Array.isArray(p?.['hydra:member'])) {
|
||||||
return payload['hydra:member']
|
return p['hydra:member'] as Piece[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.data)) {
|
if (Array.isArray(p?.data)) {
|
||||||
return payload.data
|
return p.data as Piece[]
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractTotal = (payload, fallbackLength) => {
|
const extractTotal = (payload: unknown, fallbackLength: number): number => {
|
||||||
if (typeof payload?.totalItems === 'number') {
|
const p = payload as Record<string, unknown> | null
|
||||||
return payload.totalItems
|
if (typeof p?.totalItems === 'number') {
|
||||||
|
return p.totalItems
|
||||||
}
|
}
|
||||||
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
if (typeof p?.['hydra:totalItems'] === 'number') {
|
||||||
return payload['hydra:totalItems']
|
return p['hydra:totalItems']
|
||||||
}
|
}
|
||||||
return fallbackLength
|
return fallbackLength
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePieces () {
|
export function usePieces() {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const withResolvedConstructeurs = async (piece) => {
|
const withResolvedConstructeurs = async (piece: Piece): Promise<Piece> => {
|
||||||
if (!piece || typeof piece !== 'object') {
|
if (!piece || typeof piece !== 'object') {
|
||||||
return piece
|
return piece
|
||||||
}
|
}
|
||||||
@@ -68,12 +107,11 @@ export function usePieces () {
|
|||||||
const ids = uniqueConstructeurIds(
|
const ids = uniqueConstructeurIds(
|
||||||
piece.constructeurIds,
|
piece.constructeurIds,
|
||||||
piece.constructeurs,
|
piece.constructeurs,
|
||||||
piece.constructeur,
|
|
||||||
)
|
)
|
||||||
const hasResolvedConstructeurs =
|
const hasResolvedConstructeurs =
|
||||||
Array.isArray(piece.constructeurs)
|
Array.isArray(piece.constructeurs) &&
|
||||||
&& piece.constructeurs.length > 0
|
piece.constructeurs.length > 0 &&
|
||||||
&& piece.constructeurs.every((item) => item && typeof item === 'object')
|
piece.constructeurs.every((item) => item && typeof item === 'object')
|
||||||
|
|
||||||
if (ids.length && !hasResolvedConstructeurs) {
|
if (ids.length && !hasResolvedConstructeurs) {
|
||||||
const resolved = await ensureConstructeurs(ids)
|
const resolved = await ensureConstructeurs(ids)
|
||||||
@@ -85,16 +123,7 @@ export function usePieces () {
|
|||||||
return piece
|
return piece
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const loadPieces = async (options: LoadPiecesOptions = {}): Promise<PieceListResult> => {
|
||||||
* 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 = {}) => {
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -102,7 +131,7 @@ export function usePieces () {
|
|||||||
page = 1,
|
page = 1,
|
||||||
itemsPerPage = 30,
|
itemsPerPage = 30,
|
||||||
orderBy = 'name',
|
orderBy = 'name',
|
||||||
orderDir = 'asc'
|
orderDir = 'asc',
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -110,11 +139,9 @@ export function usePieces () {
|
|||||||
params.set('page', String(page))
|
params.set('page', String(page))
|
||||||
|
|
||||||
if (search && search.trim()) {
|
if (search && search.trim()) {
|
||||||
// API Platform uses property filters
|
|
||||||
params.set('name', search.trim())
|
params.set('name', search.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Platform OrderFilter syntax: order[field]=direction
|
|
||||||
params.set(`order[${orderBy}]`, orderDir)
|
params.set(`order[${orderBy}]`, orderDir)
|
||||||
|
|
||||||
const result = await get(`/pieces?${params.toString()}`)
|
const result = await get(`/pieces?${params.toString()}`)
|
||||||
@@ -129,79 +156,84 @@ export function usePieces () {
|
|||||||
items: enrichedItems,
|
items: enrichedItems,
|
||||||
total: total.value,
|
total: total.value,
|
||||||
page,
|
page,
|
||||||
itemsPerPage
|
itemsPerPage,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result as PieceListResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du chargement des pièces:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPiece = async (pieceData) => {
|
const createPiece = async (pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
||||||
const result = await post('/pieces', normalizedPayload)
|
const result = await post('/pieces', normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data as Piece)
|
||||||
pieces.value.unshift(enriched)
|
pieces.value.unshift(enriched)
|
||||||
total.value += 1
|
total.value += 1
|
||||||
const displayName = result.data?.name
|
const definition = (pieceData as Record<string, unknown>)?.definition as Record<string, unknown> | undefined
|
||||||
|| pieceData?.definition?.name
|
const displayName =
|
||||||
|| pieceData?.name
|
(result.data as Piece)?.name ||
|
||||||
|| 'Pièce'
|
(definition?.name as string | undefined) ||
|
||||||
|
pieceData?.name ||
|
||||||
|
'Pièce'
|
||||||
showSuccess(`Pièce "${displayName}" créée avec succès`)
|
showSuccess(`Pièce "${displayName}" créée avec succès`)
|
||||||
|
return { success: true, data: enriched }
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la création de la pièce:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePieceData = async (id, pieceData) => {
|
const updatePieceData = async (id: string, pieceData: Partial<Piece>): Promise<PieceSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(pieceData))
|
||||||
const result = await patch(`/pieces/${id}`, normalizedPayload)
|
const result = await patch(`/pieces/${id}`, normalizedPayload)
|
||||||
if (result.success) {
|
if (result.success && result.data) {
|
||||||
const updated = await withResolvedConstructeurs(result.data)
|
const updated = await withResolvedConstructeurs(result.data as Piece)
|
||||||
const index = pieces.value.findIndex(piece => piece.id === id)
|
const index = pieces.value.findIndex((piece) => piece.id === id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
pieces.value[index] = updated
|
pieces.value[index] = updated
|
||||||
}
|
}
|
||||||
showSuccess(`Pièce "${updated?.name || pieceData.name || ''}" mise à jour avec succès`)
|
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) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la mise à jour de la pièce:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletePiece = async (id) => {
|
const deletePiece = async (id: string): Promise<PieceSingleResult> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await del(`/pieces/${id}`)
|
const result = await del(`/pieces/${id}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const deletedPiece = pieces.value.find(piece => piece.id === id)
|
const deletedPiece = pieces.value.find((piece) => piece.id === id)
|
||||||
pieces.value = pieces.value.filter(piece => piece.id !== id)
|
pieces.value = pieces.value.filter((piece) => piece.id !== id)
|
||||||
total.value = Math.max(0, total.value - 1)
|
total.value = Math.max(0, total.value - 1)
|
||||||
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
showSuccess(`Pièce "${deletedPiece?.name || 'inconnu'}" supprimée avec succès`)
|
||||||
|
return { success: true }
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors de la suppression de la pièce:', 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -219,6 +251,6 @@ export function usePieces () {
|
|||||||
updatePiece: updatePieceData,
|
updatePiece: updatePieceData,
|
||||||
deletePiece,
|
deletePiece,
|
||||||
getPieces,
|
getPieces,
|
||||||
isLoading
|
isLoading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,34 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
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)
|
const loadingProductTypes = ref(false)
|
||||||
|
|
||||||
export function useProductTypes () {
|
export function useProductTypes() {
|
||||||
const { showSuccess, showError } = useToast()
|
const { showSuccess, showError } = useToast()
|
||||||
|
|
||||||
const generateCodeFromName = (name) => {
|
const generateCodeFromName = (name: string): string => {
|
||||||
return (name || '')
|
return (name || '')
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/[\u0300-\u036F]/g, '')
|
.replace(/[\u0300-\u036F]/g, '')
|
||||||
@@ -18,7 +38,7 @@ export function useProductTypes () {
|
|||||||
.replace(/-+/g, '-') || 'type'
|
.replace(/-+/g, '-') || 'type'
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadProductTypes = async () => {
|
const loadProductTypes = async (): Promise<ProductTypeResult> => {
|
||||||
loadingProductTypes.value = true
|
loadingProductTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await listModelTypes({
|
const data = await listModelTypes({
|
||||||
@@ -28,14 +48,16 @@ export function useProductTypes () {
|
|||||||
limit: 200,
|
limit: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
productTypes.value = data.items.map(item => ({
|
productTypes.value = data.items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
structure: item.structure as ProductModelStructure | null,
|
||||||
description: item.description ?? item.notes ?? null,
|
description: item.description ?? item.notes ?? null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return { success: true, data: productTypes.value }
|
return { success: true, data: productTypes.value }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Impossible de charger les types de produit: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -43,20 +65,21 @@ export function useProductTypes () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createProductType = async (payload) => {
|
const createProductType = async (payload: ProductTypePayload): Promise<ProductTypeResult> => {
|
||||||
loadingProductTypes.value = true
|
loadingProductTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await createModelType({
|
const data = await createModelType({
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
code: payload.code || generateCodeFromName(payload.name),
|
code: payload.code || generateCodeFromName(payload.name),
|
||||||
category: 'PRODUCT',
|
category: 'PRODUCT',
|
||||||
notes: payload.description ?? payload.notes,
|
notes: payload.description ?? payload.notes ?? undefined,
|
||||||
description: payload.description ?? null,
|
description: payload.description ?? undefined,
|
||||||
structure: payload.structure,
|
structure: payload.structure ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalized = {
|
const normalized: ProductType = {
|
||||||
...data,
|
...data,
|
||||||
|
structure: data.structure as ProductModelStructure | null,
|
||||||
description: data.description ?? data.notes ?? null,
|
description: data.description ?? data.notes ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +88,8 @@ export function useProductTypes () {
|
|||||||
|
|
||||||
return { success: true, data: normalized }
|
return { success: true, data: normalized }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la création du type de produit: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} 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
|
loadingProductTypes.value = true
|
||||||
try {
|
try {
|
||||||
const data = await updateModelType(id, {
|
const data = await updateModelType(id, {
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
description: payload.description,
|
description: payload.description ?? undefined,
|
||||||
notes: payload.notes,
|
notes: payload.notes ?? undefined,
|
||||||
code: payload.code,
|
code: payload.code,
|
||||||
structure: payload.structure,
|
structure: payload.structure ?? undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const normalized = {
|
const normalized: ProductType = {
|
||||||
...data,
|
...data,
|
||||||
|
structure: data.structure as ProductModelStructure | null,
|
||||||
description: data.description ?? data.notes ?? 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) {
|
if (index !== -1) {
|
||||||
productTypes.value[index] = normalized
|
productTypes.value[index] = normalized
|
||||||
}
|
}
|
||||||
@@ -97,7 +122,8 @@ export function useProductTypes () {
|
|||||||
|
|
||||||
return { success: true, data: normalized }
|
return { success: true, data: normalized }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la mise à jour du type de produit: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -105,15 +131,16 @@ export function useProductTypes () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteProductType = async (id) => {
|
const deleteProductType = async (id: string): Promise<ProductTypeResult> => {
|
||||||
loadingProductTypes.value = true
|
loadingProductTypes.value = true
|
||||||
try {
|
try {
|
||||||
await deleteModelType(id)
|
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é')
|
showSuccess('Type de produit supprimé')
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} 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}`)
|
showError(`Erreur lors de la suppression du type de produit: ${message}`)
|
||||||
return { success: false, error: message }
|
return { success: false, error: message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2,16 +2,52 @@ import { ref } from 'vue'
|
|||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { useConstructeurs } from './useConstructeurs'
|
import { useConstructeurs, type Constructeur } from './useConstructeurs'
|
||||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
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 total = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loaded = 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) {
|
if (!item?.id) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -26,38 +62,40 @@ const replaceInCache = (item) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractCollection = (payload) => {
|
const extractCollection = (payload: unknown): Product[] => {
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) {
|
||||||
return payload
|
return payload as Product[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.member)) {
|
const p = payload as Record<string, unknown> | null
|
||||||
return payload.member
|
if (Array.isArray(p?.member)) {
|
||||||
|
return p.member as Product[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.['hydra:member'])) {
|
if (Array.isArray(p?.['hydra:member'])) {
|
||||||
return payload['hydra:member']
|
return p['hydra:member'] as Product[]
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.data)) {
|
if (Array.isArray(p?.data)) {
|
||||||
return payload.data
|
return p.data as Product[]
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractTotal = (payload, fallbackLength) => {
|
const extractTotal = (payload: unknown, fallbackLength: number): number => {
|
||||||
if (typeof payload?.totalItems === 'number') {
|
const p = payload as Record<string, unknown> | null
|
||||||
return payload.totalItems
|
if (typeof p?.totalItems === 'number') {
|
||||||
|
return p.totalItems
|
||||||
}
|
}
|
||||||
if (typeof payload?.['hydra:totalItems'] === 'number') {
|
if (typeof p?.['hydra:totalItems'] === 'number') {
|
||||||
return payload['hydra:totalItems']
|
return p['hydra:totalItems']
|
||||||
}
|
}
|
||||||
return fallbackLength
|
return fallbackLength
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProducts () {
|
export function useProducts() {
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
const { ensureConstructeurs } = useConstructeurs()
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const withResolvedConstructeurs = async (product) => {
|
const withResolvedConstructeurs = async (product: Product): Promise<Product> => {
|
||||||
if (!product || typeof product !== 'object') {
|
if (!product || typeof product !== 'object') {
|
||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
@@ -70,12 +108,11 @@ export function useProducts () {
|
|||||||
const ids = uniqueConstructeurIds(
|
const ids = uniqueConstructeurIds(
|
||||||
product.constructeurIds,
|
product.constructeurIds,
|
||||||
product.constructeurs,
|
product.constructeurs,
|
||||||
product.constructeur,
|
|
||||||
)
|
)
|
||||||
const hasResolvedConstructeurs =
|
const hasResolvedConstructeurs =
|
||||||
Array.isArray(product.constructeurs)
|
Array.isArray(product.constructeurs) &&
|
||||||
&& product.constructeurs.length > 0
|
product.constructeurs.length > 0 &&
|
||||||
&& product.constructeurs.every((item) => item && typeof item === 'object')
|
product.constructeurs.every((item) => item && typeof item === 'object')
|
||||||
|
|
||||||
if (ids.length && !hasResolvedConstructeurs) {
|
if (ids.length && !hasResolvedConstructeurs) {
|
||||||
const resolved = await ensureConstructeurs(ids)
|
const resolved = await ensureConstructeurs(ids)
|
||||||
@@ -87,24 +124,13 @@ export function useProducts () {
|
|||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const loadProducts = async (options: LoadProductsOptions = {}): Promise<ProductListResult> => {
|
||||||
* 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 {
|
const {
|
||||||
search = '',
|
search = '',
|
||||||
page = 1,
|
page = 1,
|
||||||
itemsPerPage = 30,
|
itemsPerPage = 30,
|
||||||
orderBy = 'name',
|
orderBy = 'name',
|
||||||
orderDir = 'asc',
|
orderDir = 'asc',
|
||||||
force = false
|
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
@@ -140,17 +166,17 @@ export function useProducts () {
|
|||||||
items: enrichedItems,
|
items: enrichedItems,
|
||||||
total: total.value,
|
total: total.value,
|
||||||
page,
|
page,
|
||||||
itemsPerPage
|
itemsPerPage,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
showError(`Impossible de charger les produits: ${result.error}`)
|
showError(`Impossible de charger les produits: ${result.error}`)
|
||||||
}
|
}
|
||||||
return result
|
return result as ProductListResult
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur lors du chargement des produits:', 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
|
error.value = message
|
||||||
showError(`Impossible de charger les produits: ${message}`)
|
showError(`Impossible de charger les produits: ${message}`)
|
||||||
return { success: false, error: 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))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await post('/products', normalizedPayload)
|
const result = await post('/products', normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data as Product)
|
||||||
const added = replaceInCache(enriched)
|
const added = replaceInCache(enriched)
|
||||||
if (added) {
|
if (added) {
|
||||||
total.value += 1
|
total.value += 1
|
||||||
}
|
}
|
||||||
|
return { success: true, data: enriched }
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur lors de la création du produit:', 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
|
error.value = message
|
||||||
showError(message)
|
showError(message)
|
||||||
return { success: false, error: 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))
|
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/products/${id}`, normalizedPayload)
|
const result = await patch(`/products/${id}`, normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data as Product)
|
||||||
replaceInCache(enriched)
|
replaceInCache(enriched)
|
||||||
|
return { success: true, data: enriched }
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur lors de la mise à jour du produit:', 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
|
error.value = message
|
||||||
showError(message)
|
showError(message)
|
||||||
return { success: false, error: 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
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await del(`/products/${id}`)
|
const result = await del(`/products/${id}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const removed = products.value.find((product) => product.id === id)
|
|
||||||
products.value = products.value.filter((product) => product.id !== id)
|
products.value = products.value.filter((product) => product.id !== id)
|
||||||
total.value = Math.max(0, total.value - 1)
|
total.value = Math.max(0, total.value - 1)
|
||||||
|
return { success: true }
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur lors de la suppression du produit:', 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
|
error.value = message
|
||||||
showError(message)
|
showError(message)
|
||||||
return { success: false, error: 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
|
const shouldForce = !!options.force
|
||||||
if (!shouldForce) {
|
if (!shouldForce) {
|
||||||
const cached = products.value.find((product) => product.id === id)
|
const cached = products.value.find((product) => product.id === id)
|
||||||
@@ -249,14 +277,14 @@ export function useProducts () {
|
|||||||
try {
|
try {
|
||||||
const result = await get(`/products/${id}`)
|
const result = await get(`/products/${id}`)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const enriched = await withResolvedConstructeurs(result.data)
|
const enriched = await withResolvedConstructeurs(result.data as Product)
|
||||||
replaceInCache(enriched)
|
replaceInCache(enriched)
|
||||||
return { success: true, data: enriched }
|
return { success: true, data: enriched }
|
||||||
}
|
}
|
||||||
return result
|
return { success: false, error: result.error }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur lors du chargement du produit:', 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 }
|
return { success: false, error: message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,8 +166,9 @@ export function useSiteManagement() {
|
|||||||
)
|
)
|
||||||
uploadingDocuments.value = false
|
uploadingDocuments.value = false
|
||||||
|
|
||||||
if (uploadResult.success) {
|
if (uploadResult.success && uploadResult.data) {
|
||||||
uploadedDocuments = uploadResult.data || []
|
const data = uploadResult.data
|
||||||
|
uploadedDocuments = (Array.isArray(data) ? data : [data]) as SiteDocument[]
|
||||||
selectedFiles.value = []
|
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 { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import { canPreviewDocument, isImageDocument, isPdfDocument } 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 {
|
interface ComponentCatalogType extends ModelType {
|
||||||
structure: ComponentModelStructure | null
|
structure: ComponentModelStructure | null
|
||||||
customFields?: Array<Record<string, any>>
|
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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { get } = useApi()
|
const { get } = useApi()
|
||||||
@@ -601,75 +609,8 @@ const historyFieldLabels: Record<string, string> = {
|
|||||||
constructeurIds: 'Fournisseurs',
|
constructeurIds: 'Fournisseurs',
|
||||||
}
|
}
|
||||||
|
|
||||||
const historyActionLabel = (action: string) => {
|
const historyDiffEntries = (entry: ComponentHistoryEntry) =>
|
||||||
if (action === 'create') {
|
_historyDiffEntries(entry, historyFieldLabels)
|
||||||
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 selectedTypeId = ref<string>('')
|
const selectedTypeId = ref<string>('')
|
||||||
const editionForm = reactive({
|
const editionForm = reactive({
|
||||||
name: '' as string,
|
name: '' as string,
|
||||||
@@ -679,49 +620,6 @@ const editionForm = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
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 fetchedPieceTypeMap = ref<Record<string, string>>({})
|
||||||
const pieceTypeLabelMap = computed(() => ({
|
const pieceTypeLabelMap = computed(() => ({
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
@@ -761,12 +659,6 @@ const componentCatalogMap = computed(() =>
|
|||||||
.map((item: any) => [String(item.id), item]),
|
.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) => {
|
const openPreview = (doc: any) => {
|
||||||
if (!doc || !canPreviewDocument(doc)) {
|
if (!doc || !canPreviewDocument(doc)) {
|
||||||
return
|
return
|
||||||
@@ -778,20 +670,6 @@ const closePreview = () => {
|
|||||||
previewVisible.value = false
|
previewVisible.value = false
|
||||||
previewDocument.value = null
|
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) => {
|
const removeDocument = async (documentId: string | number | null | undefined) => {
|
||||||
if (!documentId) {
|
if (!documentId) {
|
||||||
return
|
return
|
||||||
@@ -865,15 +743,7 @@ const refreshCustomFieldInputs = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||||
if (!field.required) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (field.type === 'boolean') {
|
|
||||||
return field.value === 'true' || field.value === 'false'
|
|
||||||
}
|
|
||||||
return toFieldString(field.value).trim() !== ''
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const canSubmit = computed(() => Boolean(
|
const canSubmit = computed(() => Boolean(
|
||||||
@@ -883,19 +753,6 @@ const canSubmit = computed(() => Boolean(
|
|||||||
!saving.value,
|
!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 fetchComponent = async () => {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
if (!id || typeof id !== 'string') {
|
if (!id || typeof id !== 'string') {
|
||||||
@@ -996,7 +853,15 @@ const submitEdition = async () => {
|
|||||||
const result = await updateComposant(component.value.id, payload)
|
const result = await updateComposant(component.value.id, payload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updatedComponent = result.data
|
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')
|
await router.push('/component-catalog')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} 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) => {
|
const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
|
||||||
return Array.isArray(structure?.customFields) ? structure.customFields : []
|
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 () => {
|
onMounted(async () => {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
loadComponentTypes(),
|
loadComponentTypes(),
|
||||||
|
|||||||
@@ -360,6 +360,10 @@ import { useToast } from '~/composables/useToast'
|
|||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||||
|
import {
|
||||||
|
toFieldString,
|
||||||
|
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||||
|
} from '~/shared/utils/customFieldFormUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type {
|
import type {
|
||||||
ComponentModelPiece,
|
ComponentModelPiece,
|
||||||
@@ -747,15 +751,7 @@ const serializeStructureAssignments = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||||
if (!field.required) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (field.type === 'boolean') {
|
|
||||||
return field.value === 'true' || field.value === 'false'
|
|
||||||
}
|
|
||||||
return toFieldString(field.value).trim() !== ''
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const canSubmit = computed(() => Boolean(
|
const canSubmit = computed(() => Boolean(
|
||||||
@@ -766,19 +762,6 @@ const canSubmit = computed(() => Boolean(
|
|||||||
!submitting.value,
|
!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) => {
|
const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
|
||||||
return Array.isArray(structure?.customFields) ? structure.customFields : []
|
return Array.isArray(structure?.customFields) ? structure.customFields : []
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -479,31 +479,39 @@ import { useDocuments } from '~/composables/useDocuments'
|
|||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { usePieceHistory, type PieceHistoryEntry } from '~/composables/usePieceHistory'
|
import { usePieceHistory, type PieceHistoryEntry } from '~/composables/usePieceHistory'
|
||||||
import { extractRelationId } from '~/shared/apiRelations'
|
import { extractRelationId } from '~/shared/apiRelations'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
import { getModelType } 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 {
|
interface PieceCatalogType extends ModelType {
|
||||||
structure: PieceModelStructure | null
|
structure: PieceModelStructure | null
|
||||||
customFields?: Array<Record<string, any>>
|
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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { get } = useApi()
|
const { get } = useApi()
|
||||||
@@ -542,75 +550,8 @@ const historyFieldLabels: Record<string, string> = {
|
|||||||
constructeurIds: 'Fournisseurs',
|
constructeurIds: 'Fournisseurs',
|
||||||
}
|
}
|
||||||
|
|
||||||
const historyActionLabel = (action: string) => {
|
const historyDiffEntries = (entry: PieceHistoryEntry) =>
|
||||||
if (action === 'create') {
|
_historyDiffEntries(entry, historyFieldLabels)
|
||||||
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 selectedTypeId = ref<string>('')
|
const selectedTypeId = ref<string>('')
|
||||||
const pieceTypeDetails = ref<any | null>(null)
|
const pieceTypeDetails = ref<any | null>(null)
|
||||||
@@ -623,8 +564,6 @@ const editionForm = reactive({
|
|||||||
const productSelections = ref<(string | null)[]>([])
|
const productSelections = ref<(string | null)[]>([])
|
||||||
|
|
||||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
const documentIcon = (doc: any) =>
|
|
||||||
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
|
||||||
const resolvedStructure = computed<PieceModelStructure | null>(() =>
|
const resolvedStructure = computed<PieceModelStructure | null>(() =>
|
||||||
pieceTypeDetails.value?.structure ?? selectedType.value?.structure ?? null,
|
pieceTypeDetails.value?.structure ?? selectedType.value?.structure ?? null,
|
||||||
)
|
)
|
||||||
@@ -637,52 +576,6 @@ const refreshCustomFieldInputs = (
|
|||||||
const values = valuesOverride ?? piece.value?.customFieldValues ?? null
|
const values = valuesOverride ?? piece.value?.customFieldValues ?? null
|
||||||
customFieldInputs.value = buildCustomFieldInputs(structure, values)
|
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) => {
|
const openPreview = (doc: any) => {
|
||||||
if (!doc || !canPreviewDocument(doc)) {
|
if (!doc || !canPreviewDocument(doc)) {
|
||||||
return
|
return
|
||||||
@@ -694,20 +587,6 @@ const closePreview = () => {
|
|||||||
previewVisible.value = false
|
previewVisible.value = false
|
||||||
previewDocument.value = null
|
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) => {
|
const removeDocument = async (documentId: string | number | null | undefined) => {
|
||||||
if (!documentId) {
|
if (!documentId) {
|
||||||
return
|
return
|
||||||
@@ -848,15 +727,7 @@ watch(structureProducts, (products) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||||
if (!field.required) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (field.type === 'boolean') {
|
|
||||||
return field.value === 'true' || field.value === 'false'
|
|
||||||
}
|
|
||||||
return toFieldString(field.value).trim() !== ''
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
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 fetchPiece = async () => {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
if (!id || typeof id !== 'string') {
|
if (!id || typeof id !== 'string') {
|
||||||
@@ -1042,7 +900,15 @@ const submitEdition = async () => {
|
|||||||
const result = await updatePiece(piece.value.id, payload)
|
const result = await updatePiece(piece.value.id, payload)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updatedPiece = result.data
|
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')
|
await router.push('/pieces-catalog')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} 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 () => {
|
onMounted(async () => {
|
||||||
await Promise.allSettled([loadPieceTypes(), fetchPiece()])
|
await Promise.allSettled([loadPieceTypes(), fetchPiece()])
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
@@ -308,6 +308,13 @@ import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
|||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
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 {
|
interface PieceCatalogType extends ModelType {
|
||||||
structure: PieceModelStructure | null
|
structure: PieceModelStructure | null
|
||||||
@@ -466,15 +473,7 @@ watch(selectedType, (type) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||||
if (!field.required) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (field.type === 'boolean') {
|
|
||||||
return field.value === 'true' || field.value === 'false'
|
|
||||||
}
|
|
||||||
return toFieldString(field.value).trim() !== ''
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
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 = () => {
|
const clearCreationForm = () => {
|
||||||
creationForm.name = ''
|
creationForm.name = ''
|
||||||
creationForm.reference = ''
|
creationForm.reference = ''
|
||||||
@@ -558,7 +544,15 @@ const submitCreation = async () => {
|
|||||||
try {
|
try {
|
||||||
const result = await createPiece(payload)
|
const result = await createPiece(payload)
|
||||||
if (result.success) {
|
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) {
|
if (selectedDocuments.value.length && result.data?.id) {
|
||||||
uploadingDocuments.value = true
|
uploadingDocuments.value = true
|
||||||
const uploadResult = await uploadDocuments(
|
const uploadResult = await uploadDocuments(
|
||||||
@@ -593,225 +587,4 @@ onMounted(async () => {
|
|||||||
await loadPieceTypes()
|
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>
|
</script>
|
||||||
|
|||||||
@@ -402,20 +402,28 @@ import { formatProductStructurePreview, normalizeProductStructureForSave } from
|
|||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { getModelType } from '~/services/modelTypes'
|
import { getModelType } from '~/services/modelTypes'
|
||||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import {
|
||||||
|
type CustomFieldInput,
|
||||||
interface CustomFieldInput {
|
fieldKey,
|
||||||
id: string | null
|
buildCustomFieldInputs,
|
||||||
name: string
|
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||||
type: string
|
saveCustomFieldValues as _saveCustomFieldValues,
|
||||||
required: boolean
|
} from '~/shared/utils/customFieldFormUtils'
|
||||||
options: string[]
|
import {
|
||||||
value: string
|
documentIcon,
|
||||||
customFieldId: string | null
|
formatSize,
|
||||||
customFieldValueId: string | null
|
shouldInlinePdf,
|
||||||
orderIndex: number
|
documentPreviewSrc,
|
||||||
}
|
documentThumbnailClass,
|
||||||
|
downloadDocument,
|
||||||
|
} from '~/shared/utils/documentDisplayUtils'
|
||||||
|
import {
|
||||||
|
historyActionLabel,
|
||||||
|
formatHistoryDate,
|
||||||
|
formatHistoryValue,
|
||||||
|
historyDiffEntries as _historyDiffEntries,
|
||||||
|
} from '~/shared/utils/historyDisplayUtils'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -458,75 +466,8 @@ const historyFieldLabels: Record<string, string> = {
|
|||||||
constructeurIds: 'Fournisseurs',
|
constructeurIds: 'Fournisseurs',
|
||||||
}
|
}
|
||||||
|
|
||||||
const historyActionLabel = (action: string) => {
|
const historyDiffEntries = (entry: ProductHistoryEntry) =>
|
||||||
if (action === 'create') {
|
_historyDiffEntries(entry, historyFieldLabels)
|
||||||
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 refreshCustomFieldInputs = (
|
const refreshCustomFieldInputs = (
|
||||||
structureOverride?: ProductModelStructure | null,
|
structureOverride?: ProductModelStructure | null,
|
||||||
@@ -545,15 +486,7 @@ const editionForm = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
customFieldInputs.value.every((field) => {
|
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||||
if (!field.required) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (field.type === 'boolean') {
|
|
||||||
return field.value === 'true' || field.value === 'false'
|
|
||||||
}
|
|
||||||
return field.value.trim().length > 0
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
@@ -562,60 +495,6 @@ const canSubmit = computed(() =>
|
|||||||
|
|
||||||
const structurePreview = computed(() => formatProductStructurePreview(structure.value))
|
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) => {
|
const openPreview = (doc: any) => {
|
||||||
if (!doc || !canPreviewDocument(doc)) {
|
if (!doc || !canPreviewDocument(doc)) {
|
||||||
@@ -630,20 +509,6 @@ const closePreview = () => {
|
|||||||
previewDocument.value = null
|
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 loadProduct = async () => {
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
@@ -768,86 +633,6 @@ watch(
|
|||||||
{ immediate: true },
|
{ 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 () => {
|
const submitEdition = async () => {
|
||||||
if (!product.value) {
|
if (!product.value) {
|
||||||
return
|
return
|
||||||
@@ -875,7 +660,12 @@ const submitEdition = async () => {
|
|||||||
const result = await updateProduct(product.value.id, payload)
|
const result = await updateProduct(product.value.id, payload)
|
||||||
if (result.success && result.data?.id) {
|
if (result.success && result.data?.id) {
|
||||||
product.value = result.data
|
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) {
|
if (failedFields.length) {
|
||||||
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
||||||
return
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadProduct()
|
await loadProduct()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -249,6 +249,10 @@ import { formatProductStructurePreview, normalizeProductStructureForSave } from
|
|||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||||
import type { ModelType } from '~/services/modelTypes'
|
import type { ModelType } from '~/services/modelTypes'
|
||||||
|
import {
|
||||||
|
type CustomFieldInput,
|
||||||
|
normalizeCustomFieldInputs as normalizeCustomFieldInputsFromUtils,
|
||||||
|
} from '~/shared/utils/customFieldFormUtils'
|
||||||
|
|
||||||
interface ProductCatalogType extends ModelType {
|
interface ProductCatalogType extends ModelType {
|
||||||
structure: ProductModelStructure | null
|
structure: ProductModelStructure | null
|
||||||
@@ -276,17 +280,6 @@ const creationForm = reactive({
|
|||||||
const selectedDocuments = ref<File[]>([])
|
const selectedDocuments = ref<File[]>([])
|
||||||
const uploadingDocuments = ref(false)
|
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 customFieldInputs = ref<CustomFieldInput[]>([])
|
||||||
|
|
||||||
const loadingTypes = computed(() => loadingProductTypes.value)
|
const loadingTypes = computed(() => loadingProductTypes.value)
|
||||||
@@ -337,7 +330,7 @@ watch(selectedType, (type) => {
|
|||||||
if (!creationForm.name) {
|
if (!creationForm.name) {
|
||||||
creationForm.name = type.name
|
creationForm.name = type.name
|
||||||
}
|
}
|
||||||
customFieldInputs.value = normalizeCustomFieldInputs(normalizeProductStructureForSave(type.structure))
|
customFieldInputs.value = normalizeCustomFieldInputsFromUtils(normalizeProductStructureForSave(type.structure))
|
||||||
})
|
})
|
||||||
|
|
||||||
const requiredCustomFieldsFilled = computed(() =>
|
const requiredCustomFieldsFilled = computed(() =>
|
||||||
@@ -362,49 +355,6 @@ const canSubmit = computed(() => Boolean(
|
|||||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||||
field.customFieldId || field.id || `${field.name}-${index}`
|
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 = () => {
|
const clearForm = () => {
|
||||||
creationForm.name = ''
|
creationForm.name = ''
|
||||||
creationForm.reference = ''
|
creationForm.reference = ''
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ function resolveBaseUrl() {
|
|||||||
return runtimeConfig.public.apiBaseUrl || '';
|
return runtimeConfig.public.apiBaseUrl || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOptions<T>(options: FetchOptions<T> = {}) {
|
function createOptions(options: Record<string, unknown> = {}): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
baseURL: resolveBaseUrl(),
|
baseURL: resolveBaseUrl(),
|
||||||
credentials: 'include' as const,
|
credentials: 'include',
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const extractRelationId = (value: unknown): string | null => {
|
|||||||
}
|
}
|
||||||
if (trimmed.includes('/')) {
|
if (trimmed.includes('/')) {
|
||||||
const parts = trimmed.split('/').filter(Boolean);
|
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;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const toStringId = (value: unknown): string | null => {
|
|||||||
}
|
}
|
||||||
if (trimmed.includes('/')) {
|
if (trimmed.includes('/')) {
|
||||||
const parts = trimmed.split('/').filter(Boolean);
|
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;
|
return trimmed;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ const sanitizeCustomFields = (fields: any[]): ComponentModelCustomField[] => {
|
|||||||
if (!options && typeof field?.optionsText === 'string') {
|
if (!options && typeof field?.optionsText === 'string') {
|
||||||
const parsedFromText = field.optionsText
|
const parsedFromText = field.optionsText
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((option) => option.trim())
|
.map((option: string) => option.trim())
|
||||||
.filter((option) => option.length > 0)
|
.filter((option: string) => option.length > 0)
|
||||||
options = parsedFromText.length ? parsedFromText : undefined
|
options = parsedFromText.length ? parsedFromText : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -917,8 +917,8 @@ const sanitizePieceCustomFields = (fields: any[]): PieceModelCustomField[] => {
|
|||||||
: ''
|
: ''
|
||||||
const parsed = rawOptions
|
const parsed = rawOptions
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((option) => option.trim())
|
.map((option: string) => option.trim())
|
||||||
.filter((option) => option.length > 0)
|
.filter((option: string) => option.length > 0)
|
||||||
options = parsed.length > 0 ? parsed : undefined
|
options = parsed.length > 0 ? parsed : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,7 +944,7 @@ export const normalizePieceStructureForSave = (input: any): PieceModelStructure
|
|||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...Object.fromEntries(restEntries),
|
...Object.fromEntries(restEntries),
|
||||||
products: sanitizePieceProducts(source.products),
|
products: sanitizePieceProducts(source.products || []),
|
||||||
customFields: sanitizePieceCustomFields(source.customFields),
|
customFields: sanitizePieceCustomFields(source.customFields),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -974,7 +974,7 @@ export const hydratePieceStructureForEditor = (input: any): PieceModelStructureF
|
|||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
Object.entries(source).filter(([key]) => key !== 'customFields' && key !== 'products'),
|
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),
|
customFields: hydratePieceCustomFields(source.customFields),
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface ComponentModelCustomField {
|
|||||||
id?: string
|
id?: string
|
||||||
customFieldId?: string
|
customFieldId?: string
|
||||||
orderIndex?: number
|
orderIndex?: number
|
||||||
|
key?: string
|
||||||
|
value?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComponentModelPiece {
|
export interface ComponentModelPiece {
|
||||||
@@ -52,6 +54,9 @@ export interface PieceModelCustomField {
|
|||||||
required: boolean
|
required: boolean
|
||||||
options?: string[]
|
options?: string[]
|
||||||
orderIndex?: number
|
orderIndex?: number
|
||||||
|
key?: string
|
||||||
|
value?: unknown
|
||||||
|
defaultValue?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PieceModelProduct {
|
export interface PieceModelProduct {
|
||||||
@@ -184,7 +189,7 @@ const validateStructureNode = (
|
|||||||
: []
|
: []
|
||||||
|
|
||||||
const subcomponents: ComponentModelStructureNode[] = []
|
const subcomponents: ComponentModelStructureNode[] = []
|
||||||
rawSubcomponents.forEach((subValue, index) => {
|
rawSubcomponents.forEach((subValue: unknown, index: number) => {
|
||||||
const parsed = validateStructureNode(subValue, issues, `${path}.subcomponents[${index}]`)
|
const parsed = validateStructureNode(subValue, issues, `${path}.subcomponents[${index}]`)
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
subcomponents.push(parsed)
|
subcomponents.push(parsed)
|
||||||
@@ -260,6 +265,7 @@ export const componentModelStructureValidator = {
|
|||||||
...node,
|
...node,
|
||||||
customFields,
|
customFields,
|
||||||
pieces,
|
pieces,
|
||||||
|
products: ((node as unknown) as Record<string, unknown>).products as ComponentModelProduct[] ?? [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
220
app/shared/utils/assignmentUtils.ts
Normal file
220
app/shared/utils/assignmentUtils.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* Entity assignment normalization and display utilities.
|
||||||
|
*
|
||||||
|
* Extracted from pages/machines/new.vue – these pure functions resolve
|
||||||
|
* machine / component / piece assignments from nested API payloads.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type AnyRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Primitive helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const isPlainObject = (value: unknown): value is AnyRecord =>
|
||||||
|
value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
|
||||||
|
const toTrimmedString = (value: unknown): string | null => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
return trimmed.length > 0 ? trimmed : null
|
||||||
|
}
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dedup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const dedupeAssignments = (
|
||||||
|
assignments: AnyRecord[],
|
||||||
|
): AnyRecord[] => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return assignments.filter((assignment) => {
|
||||||
|
if (!assignment) return false
|
||||||
|
const id = assignment.id != null ? String(assignment.id) : ''
|
||||||
|
const name = assignment.name != null ? String(assignment.name) : ''
|
||||||
|
const key = `${id}::${name}`
|
||||||
|
if (!id && !name) return false
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Machine assignments
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const normalizeMachineAssignment = (input: unknown): AnyRecord | null => {
|
||||||
|
if (!input) return null
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
const name = toTrimmedString(input)
|
||||||
|
return name ? { id: name, name } : null
|
||||||
|
}
|
||||||
|
if (typeof input === 'number' && Number.isFinite(input)) {
|
||||||
|
const value = String(input)
|
||||||
|
return { id: value, name: value }
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = (input as AnyRecord).machine || (input as AnyRecord).machineData || input
|
||||||
|
if (!isPlainObject(container)) return null
|
||||||
|
|
||||||
|
const id =
|
||||||
|
container.id ?? (input as AnyRecord).machineId ?? (input as AnyRecord).id ?? null
|
||||||
|
const name =
|
||||||
|
container.name ||
|
||||||
|
(input as AnyRecord).machineName ||
|
||||||
|
container.label ||
|
||||||
|
container.title ||
|
||||||
|
(typeof id === 'string' ? id : null) ||
|
||||||
|
(typeof id === 'number' ? String(id) : null)
|
||||||
|
|
||||||
|
if (id == null && name == null) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id != null ? id : null,
|
||||||
|
name: name != null ? name : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const collectMachineAssignments = (source: unknown): AnyRecord[] => {
|
||||||
|
if (!isPlainObject(source)) return []
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
source.machines,
|
||||||
|
source.machineLinks,
|
||||||
|
source.machineAssignments,
|
||||||
|
source.machinesAssignments,
|
||||||
|
source.linkedMachines,
|
||||||
|
]
|
||||||
|
|
||||||
|
const assignments: AnyRecord[] = []
|
||||||
|
|
||||||
|
candidates.forEach((list) => {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
list.forEach((item) => {
|
||||||
|
const normalized = normalizeMachineAssignment(item)
|
||||||
|
if (normalized) assignments.push(normalized)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!assignments.length) {
|
||||||
|
const direct = normalizeMachineAssignment(source.machine)
|
||||||
|
if (direct) assignments.push(direct)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assignments.length) {
|
||||||
|
const idCandidate = source.machineId ?? source.machineID ?? null
|
||||||
|
const nameCandidate = source.machineName ?? null
|
||||||
|
const normalized = normalizeMachineAssignment(nameCandidate || idCandidate)
|
||||||
|
if (normalized) assignments.push(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dedupeAssignments(assignments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Component assignments
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const normalizeComponentAssignment = (input: unknown): AnyRecord | null => {
|
||||||
|
if (!input) return null
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
const value = toTrimmedString(input)
|
||||||
|
return value ? { id: value, name: value } : null
|
||||||
|
}
|
||||||
|
if (typeof input === 'number' && Number.isFinite(input)) {
|
||||||
|
const value = String(input)
|
||||||
|
return { id: value, name: value }
|
||||||
|
}
|
||||||
|
|
||||||
|
const container =
|
||||||
|
(input as AnyRecord).component || (input as AnyRecord).composant || input
|
||||||
|
if (!isPlainObject(container)) return null
|
||||||
|
|
||||||
|
const id =
|
||||||
|
container.id ??
|
||||||
|
(input as AnyRecord).componentId ??
|
||||||
|
(input as AnyRecord).composantId ??
|
||||||
|
(input as AnyRecord).id ??
|
||||||
|
null
|
||||||
|
const name =
|
||||||
|
container.name ||
|
||||||
|
(input as AnyRecord).componentName ||
|
||||||
|
(input as AnyRecord).composantName ||
|
||||||
|
container.label ||
|
||||||
|
(typeof id === 'string' ? id : null) ||
|
||||||
|
(typeof id === 'number' ? String(id) : null)
|
||||||
|
|
||||||
|
if (id == null && name == null) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id != null ? id : null,
|
||||||
|
name: name != null ? name : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const collectComponentAssignments = (source: unknown): AnyRecord[] => {
|
||||||
|
if (!isPlainObject(source)) return []
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
source.components,
|
||||||
|
source.composants,
|
||||||
|
source.componentLinks,
|
||||||
|
source.linkedComponents,
|
||||||
|
]
|
||||||
|
|
||||||
|
const assignments: AnyRecord[] = []
|
||||||
|
|
||||||
|
candidates.forEach((list) => {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
list.forEach((item) => {
|
||||||
|
const normalized = normalizeComponentAssignment(item)
|
||||||
|
if (normalized) assignments.push(normalized)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!assignments.length) {
|
||||||
|
const direct = normalizeComponentAssignment(source.component || source.composant)
|
||||||
|
if (direct) assignments.push(direct)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assignments.length) {
|
||||||
|
const idCandidate = source.componentId ?? source.composantId ?? null
|
||||||
|
const normalized = normalizeComponentAssignment(idCandidate)
|
||||||
|
if (normalized) assignments.push(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dedupeAssignments(assignments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Convenience wrappers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const getComponentMachineAssignments = (component: unknown): AnyRecord[] =>
|
||||||
|
collectMachineAssignments(component || {})
|
||||||
|
|
||||||
|
export const getPieceMachineAssignments = (piece: unknown): AnyRecord[] =>
|
||||||
|
collectMachineAssignments(piece || {})
|
||||||
|
|
||||||
|
export const getPieceComponentAssignments = (piece: unknown): AnyRecord[] =>
|
||||||
|
collectComponentAssignments(piece || {})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Display
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const formatAssignmentList = (assignments: AnyRecord[]): string => {
|
||||||
|
if (!Array.isArray(assignments) || assignments.length === 0) return ''
|
||||||
|
return assignments
|
||||||
|
.map((assignment) => (assignment?.name || assignment?.id) as string)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
440
app/shared/utils/customFieldUtils.ts
Normal file
440
app/shared/utils/customFieldUtils.ts
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
/**
|
||||||
|
* Custom field normalization, merging and display utilities.
|
||||||
|
*
|
||||||
|
* Extracted from pages/machine/[id].vue to be reusable across
|
||||||
|
* machine detail, component, piece and product views.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Primitive helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const coerceValueForType = (type: string, rawValue: unknown): string => {
|
||||||
|
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (type === 'boolean') {
|
||||||
|
const normalized = String(rawValue).toLowerCase()
|
||||||
|
if (normalized === 'true' || normalized === '1') return 'true'
|
||||||
|
if (normalized === 'false' || normalized === '0') return 'false'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return String(rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatCustomFieldValue = (field: Record<string, unknown> | null | undefined): string => {
|
||||||
|
if (!field) return 'Non défini'
|
||||||
|
|
||||||
|
const value = (field.value ?? field.defaultValue ?? '') as string
|
||||||
|
if (value === '' || value === null || value === undefined) return 'Non défini'
|
||||||
|
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
const normalized = String(value).toLowerCase()
|
||||||
|
if (normalized === 'true' || normalized === '1') return 'Oui'
|
||||||
|
if (normalized === 'false' || normalized === '0') return 'Non'
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shouldDisplayCustomField = (field: Record<string, unknown> | null | undefined): boolean => {
|
||||||
|
if (!field) return false
|
||||||
|
if (field.readOnly) return true
|
||||||
|
if (field.type === 'boolean') return field.value !== undefined && field.value !== null
|
||||||
|
|
||||||
|
const value = field.value
|
||||||
|
if (value === null || value === undefined) return false
|
||||||
|
if (typeof value === 'string') return value.trim().length > 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Definition extraction helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const extractDefinitionName = (definition: Record<string, unknown> = {}): string => {
|
||||||
|
if (typeof definition?.name === 'string' && (definition.name as string).trim()) {
|
||||||
|
return (definition.name as string).trim()
|
||||||
|
}
|
||||||
|
if (typeof definition?.key === 'string' && (definition.key as string).trim()) {
|
||||||
|
return (definition.key as string).trim()
|
||||||
|
}
|
||||||
|
if (typeof definition?.label === 'string' && (definition.label as string).trim()) {
|
||||||
|
return (definition.label as string).trim()
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractDefinitionType = (
|
||||||
|
definition: Record<string, unknown> = {},
|
||||||
|
fallback = 'text',
|
||||||
|
): string => {
|
||||||
|
const allowed = ['text', 'number', 'select', 'boolean', 'date']
|
||||||
|
const rawType =
|
||||||
|
typeof definition?.type === 'string'
|
||||||
|
? definition.type
|
||||||
|
: typeof (definition?.value as Record<string, unknown>)?.type === 'string'
|
||||||
|
? (definition.value as Record<string, unknown>).type as string
|
||||||
|
: typeof fallback === 'string'
|
||||||
|
? fallback
|
||||||
|
: 'text'
|
||||||
|
const normalized = (rawType as string).toLowerCase()
|
||||||
|
return allowed.includes(normalized) ? normalized : 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractDefinitionRequired = (
|
||||||
|
definition: Record<string, unknown> = {},
|
||||||
|
fallback = false,
|
||||||
|
): boolean => {
|
||||||
|
if (typeof definition?.required === 'boolean') return definition.required
|
||||||
|
const nested = (definition?.value as Record<string, unknown>)?.required
|
||||||
|
if (typeof nested === 'boolean') return nested
|
||||||
|
if (typeof nested === 'string') {
|
||||||
|
const normalized = nested.toLowerCase()
|
||||||
|
if (normalized === 'true' || normalized === '1') return true
|
||||||
|
if (normalized === 'false' || normalized === '0') return false
|
||||||
|
}
|
||||||
|
return !!fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractOptionList = (input: unknown): string[] | undefined => {
|
||||||
|
if (!Array.isArray(input)) return undefined
|
||||||
|
const mapped = input
|
||||||
|
.map((option) => {
|
||||||
|
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((option) => option.length > 0)
|
||||||
|
return mapped.length ? mapped : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractDefinitionOptions = (definition: Record<string, unknown> = {}): string[] => {
|
||||||
|
const sources = [
|
||||||
|
definition?.options,
|
||||||
|
(definition?.value as Record<string, unknown>)?.options,
|
||||||
|
(definition?.value as Record<string, unknown>)?.choices,
|
||||||
|
]
|
||||||
|
for (const source of sources) {
|
||||||
|
const list = extractOptionList(source)
|
||||||
|
if (list) return list
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractDefinitionDefaultValue = (definition: Record<string, unknown> = {}): unknown => {
|
||||||
|
const candidates = [
|
||||||
|
definition?.defaultValue,
|
||||||
|
(definition?.value as Record<string, unknown>)?.defaultValue,
|
||||||
|
(definition?.value as Record<string, unknown>)?.value,
|
||||||
|
definition?.value,
|
||||||
|
definition?.default,
|
||||||
|
]
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (candidate === undefined || candidate === null || candidate === '') continue
|
||||||
|
if (typeof candidate === 'object') {
|
||||||
|
if (candidate === null) continue
|
||||||
|
const nestedDefault =
|
||||||
|
(candidate as Record<string, unknown>).defaultValue !== undefined &&
|
||||||
|
(candidate as Record<string, unknown>).defaultValue !== null
|
||||||
|
? (candidate as Record<string, unknown>).defaultValue
|
||||||
|
: (candidate as Record<string, unknown>).value
|
||||||
|
if (nestedDefault !== undefined && nestedDefault !== null && nestedDefault !== '') {
|
||||||
|
return nestedDefault
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Normalization
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface NormalizedCustomFieldDefinition {
|
||||||
|
id?: string
|
||||||
|
customFieldId?: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
required: boolean
|
||||||
|
options: string[]
|
||||||
|
defaultValue?: unknown
|
||||||
|
readOnly: boolean
|
||||||
|
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,
|
||||||
|
): NormalizedCustomFieldDefinition | null => {
|
||||||
|
const name = extractDefinitionName(definition)
|
||||||
|
if (!name) return null
|
||||||
|
const type = extractDefinitionType(definition)
|
||||||
|
const required = extractDefinitionRequired(definition)
|
||||||
|
const options = extractDefinitionOptions(definition)
|
||||||
|
const defaultValue = extractDefinitionDefaultValue(definition)
|
||||||
|
const id = typeof definition?.id === 'string' ? definition.id : undefined
|
||||||
|
const customFieldId = typeof definition?.customFieldId === 'string' ? definition.customFieldId : id
|
||||||
|
const orderIndex = typeof definition?.orderIndex === 'number' ? definition.orderIndex : fallbackIndex
|
||||||
|
return { id, customFieldId, name, type, required, options, defaultValue, readOnly: !!definition?.readOnly, orderIndex }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeExistingCustomFieldDefinitions = (
|
||||||
|
fields: unknown,
|
||||||
|
): NormalizedCustomFieldDefinition[] => {
|
||||||
|
if (!Array.isArray(fields)) return []
|
||||||
|
return fields
|
||||||
|
.map((field, index) => normalizeCustomFieldDefinitionEntry(field as Record<string, unknown>, index))
|
||||||
|
.filter((d): d is NormalizedCustomFieldDefinition => d !== null)
|
||||||
|
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Custom field value normalization
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const normalizeCustomFieldValueEntry = (entry: Record<string, unknown> = {}): Record<string, unknown> | null => {
|
||||||
|
if (!entry || typeof entry !== 'object') return null
|
||||||
|
const normalizedDefinition = normalizeCustomFieldDefinitionEntry(entry)
|
||||||
|
if (!normalizedDefinition) return null
|
||||||
|
|
||||||
|
const value = coerceValueForType(
|
||||||
|
normalizedDefinition.type,
|
||||||
|
(entry?.value ?? entry?.defaultValue ?? normalizedDefinition.defaultValue ?? '') as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: (entry?.customFieldValueId ?? entry?.id ?? null) as string | null,
|
||||||
|
customFieldId:
|
||||||
|
(entry?.customFieldId ?? normalizedDefinition.customFieldId ?? normalizedDefinition.id ?? null) as string | null,
|
||||||
|
customField: {
|
||||||
|
id: normalizedDefinition.id ?? normalizedDefinition.customFieldId ?? null,
|
||||||
|
name: normalizedDefinition.name,
|
||||||
|
type: normalizedDefinition.type,
|
||||||
|
required: normalizedDefinition.required,
|
||||||
|
options: normalizedDefinition.options,
|
||||||
|
defaultValue: normalizedDefinition.defaultValue ?? '',
|
||||||
|
},
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Merge & dedup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const mergeCustomFieldValuesWithDefinitions = (
|
||||||
|
valueEntries: Record<string, unknown>[] = [],
|
||||||
|
...definitionSources: unknown[][]
|
||||||
|
): Record<string, unknown>[] => {
|
||||||
|
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>,
|
||||||
|
)
|
||||||
|
if (!normalizedDefinition) return null
|
||||||
|
|
||||||
|
const value = coerceValueForType(
|
||||||
|
normalizedDefinition.type,
|
||||||
|
((entry as Record<string, unknown>)?.value ??
|
||||||
|
(entry as Record<string, unknown>)?.defaultValue ??
|
||||||
|
normalizedDefinition.defaultValue ??
|
||||||
|
'') as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
customFieldValueId: (entry as Record<string, unknown>)?.id ?? (entry as Record<string, unknown>)?.customFieldValueId ?? null,
|
||||||
|
id: normalizedDefinition.id,
|
||||||
|
customFieldId: normalizedDefinition.customFieldId ?? normalizedDefinition.id ?? null,
|
||||||
|
name: normalizedDefinition.name,
|
||||||
|
type: normalizedDefinition.type,
|
||||||
|
required: normalizedDefinition.required,
|
||||||
|
options: normalizedDefinition.options,
|
||||||
|
optionsText: normalizedDefinition.options?.length ? normalizedDefinition.options.join('\n') : '',
|
||||||
|
defaultValue: normalizedDefinition.defaultValue ?? '',
|
||||||
|
value,
|
||||||
|
readOnly: !!(entry as Record<string, unknown>)?.readOnly,
|
||||||
|
} as Record<string, unknown>
|
||||||
|
})
|
||||||
|
.filter((entry): entry is Record<string, unknown> => entry !== null)
|
||||||
|
|
||||||
|
const result = [...normalizedValues]
|
||||||
|
const keyFor = (item: Record<string, unknown>) => (item?.id as string) ?? `${item?.name ?? ''}::${item?.type ?? ''}`
|
||||||
|
const existingMap = new Map<string, Record<string, unknown>>()
|
||||||
|
|
||||||
|
result.forEach((item) => {
|
||||||
|
const key = keyFor(item)
|
||||||
|
if (key) existingMap.set(key, item)
|
||||||
|
const fallbackKey = item?.name ? `${item.name}::${item.type ?? ''}` : null
|
||||||
|
if (fallbackKey) existingMap.set(fallbackKey, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
const definitions = definitionSources
|
||||||
|
.flatMap((source) => (Array.isArray(source) ? source : []))
|
||||||
|
.map((definition) => normalizeCustomFieldDefinitionEntry(definition as Record<string, unknown>))
|
||||||
|
.filter((d): d is NormalizedCustomFieldDefinition => d !== null)
|
||||||
|
|
||||||
|
definitions.forEach((normalizedDefinition) => {
|
||||||
|
const key = normalizedDefinition.id ?? `${normalizedDefinition.name}::${normalizedDefinition.type}`
|
||||||
|
if (!key) return
|
||||||
|
|
||||||
|
if (normalizedDefinition.id) {
|
||||||
|
const fallbackKey = `${normalizedDefinition.name}::${normalizedDefinition.type}`
|
||||||
|
if (existingMap.has(fallbackKey)) {
|
||||||
|
const existingFallback = existingMap.get(fallbackKey)
|
||||||
|
if (existingFallback) {
|
||||||
|
existingFallback.id = existingFallback.id || normalizedDefinition.id
|
||||||
|
existingFallback.customFieldId = normalizedDefinition.id
|
||||||
|
existingFallback.readOnly = (existingFallback.readOnly as boolean) && normalizedDefinition.readOnly
|
||||||
|
existingMap.delete(fallbackKey)
|
||||||
|
existingMap.set(normalizedDefinition.id, existingFallback)
|
||||||
|
existingMap.set(fallbackKey, existingFallback)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing =
|
||||||
|
existingMap.get(key) ||
|
||||||
|
(normalizedDefinition.name ? existingMap.get(`${normalizedDefinition.name}::${normalizedDefinition.type}`) : null)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.name = existing.name || normalizedDefinition.name
|
||||||
|
existing.type = existing.type || normalizedDefinition.type
|
||||||
|
existing.required = (existing.required as boolean) || normalizedDefinition.required
|
||||||
|
if (!(existing.options as string[])?.length && normalizedDefinition.options?.length) {
|
||||||
|
existing.options = normalizedDefinition.options
|
||||||
|
}
|
||||||
|
if (!existing.defaultValue && normalizedDefinition.defaultValue) {
|
||||||
|
existing.defaultValue = String(normalizedDefinition.defaultValue)
|
||||||
|
if (!existing.value) {
|
||||||
|
existing.value = coerceValueForType(existing.type as string, normalizedDefinition.defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existing.customFieldId = existing.customFieldId || normalizedDefinition.id
|
||||||
|
existing.readOnly = (existing.readOnly as boolean) && normalizedDefinition.readOnly
|
||||||
|
if (!existing.optionsText && normalizedDefinition.options?.length) {
|
||||||
|
existing.optionsText = normalizedDefinition.options.join('\n')
|
||||||
|
}
|
||||||
|
if (normalizedDefinition.id) existingMap.set(normalizedDefinition.id, existing)
|
||||||
|
if (normalizedDefinition.name) {
|
||||||
|
existingMap.set(`${normalizedDefinition.name}::${normalizedDefinition.type}`, existing)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry: Record<string, unknown> = {
|
||||||
|
customFieldValueId: null,
|
||||||
|
id: normalizedDefinition.id,
|
||||||
|
customFieldId: normalizedDefinition.id,
|
||||||
|
name: normalizedDefinition.name,
|
||||||
|
type: normalizedDefinition.type,
|
||||||
|
required: normalizedDefinition.required,
|
||||||
|
options: normalizedDefinition.options,
|
||||||
|
optionsText: normalizedDefinition.options?.length ? normalizedDefinition.options.join('\n') : '',
|
||||||
|
defaultValue: normalizedDefinition.defaultValue ?? '',
|
||||||
|
value: coerceValueForType(normalizedDefinition.type, (normalizedDefinition.defaultValue ?? '') as string),
|
||||||
|
readOnly: false,
|
||||||
|
}
|
||||||
|
result.push(entry)
|
||||||
|
existingMap.set(key, entry)
|
||||||
|
const fallbackKey = entry.name ? `${entry.name}::${entry.type}` : null
|
||||||
|
if (fallbackKey) existingMap.set(fallbackKey, entry)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dedupeCustomFieldEntries = (fields: Record<string, unknown>[]): Record<string, unknown>[] => {
|
||||||
|
if (!Array.isArray(fields) || fields.length <= 1) {
|
||||||
|
return Array.isArray(fields) ? fields : []
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const result: Record<string, unknown>[] = []
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!field) continue
|
||||||
|
|
||||||
|
field.type = field.type || 'text'
|
||||||
|
|
||||||
|
let normalizedName = typeof field.name === 'string' ? (field.name as string).trim() : ''
|
||||||
|
|
||||||
|
if (!normalizedName && (field.customField as Record<string, unknown>)?.name) {
|
||||||
|
normalizedName = String((field.customField as Record<string, unknown>).name).trim()
|
||||||
|
field.name = normalizedName
|
||||||
|
} else if (typeof field.name === 'string') {
|
||||||
|
field.name = normalizedName
|
||||||
|
}
|
||||||
|
|
||||||
|
const key =
|
||||||
|
(field.customFieldId as string) ||
|
||||||
|
(field.id as string) ||
|
||||||
|
(normalizedName ? `${normalizedName}::${field.type || 'text'}` : null)
|
||||||
|
|
||||||
|
if (!key && !normalizedName) continue
|
||||||
|
if (key && seen.has(key)) continue
|
||||||
|
if (!normalizedName) continue
|
||||||
|
|
||||||
|
if (key) seen.add(key)
|
||||||
|
if (normalizedName) seen.add(`${normalizedName}::${field.type || 'text'}`)
|
||||||
|
result.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Summarize for display
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const summarizeCustomFields = (
|
||||||
|
fields: Record<string, unknown>[] = [],
|
||||||
|
): { key: string; label: string; value: string }[] => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return fields
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
const left = typeof a?.orderIndex === 'number' ? a.orderIndex : 0
|
||||||
|
const right = typeof b?.orderIndex === 'number' ? b.orderIndex : 0
|
||||||
|
return (left as number) - (right as number)
|
||||||
|
})
|
||||||
|
.filter(shouldDisplayCustomField)
|
||||||
|
.filter((field) => {
|
||||||
|
const key = (field.customFieldId || field.id || field.name) as string
|
||||||
|
if (!key) return true
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map((field, index) => ({
|
||||||
|
key: ((field.customFieldId || field.id || field.name) as string) || `custom-field-${index}`,
|
||||||
|
label: (field.name as string) || 'Champ',
|
||||||
|
value: formatCustomFieldValue(field),
|
||||||
|
}))
|
||||||
|
}
|
||||||
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),
|
||||||
|
}))
|
||||||
|
}
|
||||||
353
app/shared/utils/productDisplayUtils.ts
Normal file
353
app/shared/utils/productDisplayUtils.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
/**
|
||||||
|
* Product resolution and display utilities.
|
||||||
|
*
|
||||||
|
* Extracted from pages/machine/[id].vue – these functions resolve product
|
||||||
|
* references from deeply nested API payloads and build display objects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface ProductDisplay {
|
||||||
|
name: string
|
||||||
|
reference: string | null
|
||||||
|
category: string | null
|
||||||
|
suppliers: string | null
|
||||||
|
price: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const isPlainObject = (value: unknown): value is AnyRecord =>
|
||||||
|
Object.prototype.toString.call(value) === '[object Object]'
|
||||||
|
|
||||||
|
export const resolveIdentifier = (...candidates: unknown[]): string | null => {
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (candidate !== undefined && candidate !== null && candidate !== '') {
|
||||||
|
return candidate as string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Supplier / price labels
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const getProductSuppliersLabel = (product: AnyRecord | null): string | null => {
|
||||||
|
if (!product) return null
|
||||||
|
const suppliers = Array.isArray(product.constructeurs)
|
||||||
|
? (product.constructeurs as AnyRecord[]).map((c) => c?.name as string).filter(Boolean)
|
||||||
|
: []
|
||||||
|
return suppliers.length > 0 ? suppliers.join(', ') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProductPriceLabel = (product: AnyRecord | null): string | null => {
|
||||||
|
if (!product) return null
|
||||||
|
const priceValue =
|
||||||
|
(product.supplierPrice ?? product.prix ?? product.price ?? null) as string | number | null
|
||||||
|
if (priceValue === undefined || priceValue === null) return null
|
||||||
|
const numeric = Number(priceValue)
|
||||||
|
if (Number.isNaN(numeric)) return null
|
||||||
|
return `${numeric.toFixed(2)} €`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// resolveProductReference
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const resolveProductReference = (
|
||||||
|
source: AnyRecord | null | undefined,
|
||||||
|
findProductById: (id: string) => AnyRecord | null,
|
||||||
|
): { product: AnyRecord | null; productId: string | null } => {
|
||||||
|
if (!source || typeof source !== 'object') {
|
||||||
|
return { product: null, productId: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateKeys: (string | null)[] = [
|
||||||
|
null,
|
||||||
|
'productLink',
|
||||||
|
'machinePieceLink',
|
||||||
|
'machineComponentLink',
|
||||||
|
'machineProductLink',
|
||||||
|
'originalPiece',
|
||||||
|
'originalComposant',
|
||||||
|
'link',
|
||||||
|
'overrides',
|
||||||
|
'machineComponentLinkOverrides',
|
||||||
|
'requirement',
|
||||||
|
'selection',
|
||||||
|
'entry',
|
||||||
|
]
|
||||||
|
|
||||||
|
let product: AnyRecord | null = null
|
||||||
|
let productId: string | null = null
|
||||||
|
|
||||||
|
const inspect = (container: unknown) => {
|
||||||
|
if (!container || typeof container !== 'object') return
|
||||||
|
const c = container as AnyRecord
|
||||||
|
if (!product && c.product && typeof c.product === 'object') {
|
||||||
|
product = c.product as AnyRecord
|
||||||
|
}
|
||||||
|
if (!productId) {
|
||||||
|
const candidate =
|
||||||
|
(c.productId as string) ||
|
||||||
|
(c.product && typeof c.product === 'object'
|
||||||
|
? ((c.product as AnyRecord).id as string) || ((c.product as AnyRecord).productId as string)
|
||||||
|
: null) ||
|
||||||
|
null
|
||||||
|
if (candidate) productId = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateKeys.forEach((key) => {
|
||||||
|
if (key === null) inspect(source)
|
||||||
|
else inspect((source as AnyRecord)[key])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!product && productId) {
|
||||||
|
product = findProductById(productId) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product && !productId && source.productName) {
|
||||||
|
const suppliersLabel =
|
||||||
|
typeof source.constructeursLabel === 'string'
|
||||||
|
? source.constructeursLabel
|
||||||
|
: typeof source.productSuppliers === 'string'
|
||||||
|
? source.productSuppliers
|
||||||
|
: null
|
||||||
|
|
||||||
|
return {
|
||||||
|
product: {
|
||||||
|
name: source.productName,
|
||||||
|
reference: source.productReference || null,
|
||||||
|
typeProduct: source.productCategory ? { name: source.productCategory } : null,
|
||||||
|
constructeurs: suppliersLabel
|
||||||
|
? (suppliersLabel as string)
|
||||||
|
.split(',')
|
||||||
|
.map((name: string) => name.trim())
|
||||||
|
.filter((name: string) => name.length > 0)
|
||||||
|
.map((name: string) => ({ name }))
|
||||||
|
: undefined,
|
||||||
|
supplierPrice: source.productPrice ?? source.productPriceLabel ?? source.price ?? null,
|
||||||
|
} as AnyRecord,
|
||||||
|
productId: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productId && product && product.id && product.id !== productId) {
|
||||||
|
const resolved = findProductById(productId)
|
||||||
|
if (resolved) product = resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
return { product: product || null, productId: productId || null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// getProductDisplay
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const getProductDisplay = (
|
||||||
|
source: AnyRecord | null | undefined,
|
||||||
|
findProductById: (id: string) => AnyRecord | null,
|
||||||
|
): ProductDisplay | null => {
|
||||||
|
if (!source || typeof source !== 'object') return null
|
||||||
|
|
||||||
|
const { product, productId } = resolveProductReference(source, findProductById)
|
||||||
|
|
||||||
|
if (product) {
|
||||||
|
return {
|
||||||
|
name: (product.name as string) || (product.reference as string) || 'Produit catalogue',
|
||||||
|
reference: (product.reference as string) || null,
|
||||||
|
category: (product.typeProduct as AnyRecord)?.name as string || null,
|
||||||
|
suppliers: getProductSuppliersLabel(product),
|
||||||
|
price: getProductPriceLabel(product),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallbackName =
|
||||||
|
(source.productName ||
|
||||||
|
source.productLabel ||
|
||||||
|
source.typeProductLabel ||
|
||||||
|
(source.typeProduct as AnyRecord)?.name ||
|
||||||
|
(productId ? `Produit ${productId}` : null)) as string | null
|
||||||
|
let fallbackReference = (source.productReference || source.reference || null) as string | null
|
||||||
|
let fallbackCategory =
|
||||||
|
(source.productCategory ||
|
||||||
|
source.typeProductLabel ||
|
||||||
|
(source.typeProduct as AnyRecord)?.name ||
|
||||||
|
null) as string | null
|
||||||
|
let fallbackSuppliers =
|
||||||
|
(source.productSuppliers ||
|
||||||
|
source.constructeursLabel ||
|
||||||
|
source.supplierLabel ||
|
||||||
|
null) as string | null
|
||||||
|
let fallbackPrice =
|
||||||
|
(source.productPriceLabel ||
|
||||||
|
source.productPrice ||
|
||||||
|
source.priceLabel ||
|
||||||
|
source.price ||
|
||||||
|
null) as string | number | null
|
||||||
|
|
||||||
|
const structuralCandidates = [
|
||||||
|
source.products,
|
||||||
|
source.productSkeleton,
|
||||||
|
(source.definition as AnyRecord)?.products,
|
||||||
|
(source.definition as AnyRecord)?.productSkeleton,
|
||||||
|
((source.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
|
((source.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||||||
|
(source.structure as AnyRecord)?.products,
|
||||||
|
(source.structure as AnyRecord)?.productSkeleton,
|
||||||
|
(source.requirement as AnyRecord)?.products,
|
||||||
|
(source.requirement as AnyRecord)?.productSkeleton,
|
||||||
|
((source.requirement as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
|
((source.requirement as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||||||
|
((source.requirement as AnyRecord)?.componentSkeleton as AnyRecord)?.products,
|
||||||
|
(source.typeMachineComponentRequirement as AnyRecord)?.products,
|
||||||
|
(source.typeMachineComponentRequirement as AnyRecord)?.productSkeleton,
|
||||||
|
((source.typeMachineComponentRequirement as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
|
((source.typeMachineComponentRequirement as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||||||
|
((source.typeMachineComponentRequirement as AnyRecord)?.componentSkeleton as AnyRecord)?.products,
|
||||||
|
(source.typeComposant as AnyRecord)?.products,
|
||||||
|
(source.typeComposant as AnyRecord)?.productSkeleton,
|
||||||
|
((source.typeComposant as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
|
((source.typeComposant as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||||||
|
(source.originalComposant as AnyRecord)?.products,
|
||||||
|
(source.originalComposant as AnyRecord)?.productSkeleton,
|
||||||
|
((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.products,
|
||||||
|
((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.productSkeleton,
|
||||||
|
(((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
|
(((source.originalComposant as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||||||
|
(source.originalComponent as AnyRecord)?.products,
|
||||||
|
(source.originalComponent as AnyRecord)?.productSkeleton,
|
||||||
|
((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.products,
|
||||||
|
((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.productSkeleton,
|
||||||
|
(((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.products,
|
||||||
|
(((source.originalComponent as AnyRecord)?.definition as AnyRecord)?.structure as AnyRecord)?.productSkeleton,
|
||||||
|
]
|
||||||
|
|
||||||
|
const structuralProducts = structuralCandidates
|
||||||
|
.flatMap((candidate) => {
|
||||||
|
if (Array.isArray(candidate)) return candidate
|
||||||
|
if (candidate && typeof candidate === 'object' && Array.isArray((candidate as AnyRecord).products)) {
|
||||||
|
return (candidate as AnyRecord).products as unknown[]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
.filter((entry) => entry && typeof entry === 'object')
|
||||||
|
|
||||||
|
const structuralProduct = structuralProducts.length ? (structuralProducts[0] as AnyRecord) : null
|
||||||
|
|
||||||
|
const structuralFamilyCode =
|
||||||
|
(structuralProduct && typeof structuralProduct.familyCode === 'string'
|
||||||
|
? structuralProduct.familyCode
|
||||||
|
: null) ||
|
||||||
|
(typeof source.familyCode === 'string' ? source.familyCode : null)
|
||||||
|
|
||||||
|
if (!fallbackName && structuralProduct) {
|
||||||
|
fallbackName =
|
||||||
|
(structuralProduct.typeProductLabel as string) ||
|
||||||
|
((structuralProduct.typeProduct as AnyRecord)?.name as string) ||
|
||||||
|
(structuralProduct.reference as string) ||
|
||||||
|
(structuralFamilyCode ? `Famille ${structuralFamilyCode}` : null) ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fallbackReference && structuralProduct?.reference) {
|
||||||
|
fallbackReference = structuralProduct.reference as string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fallbackCategory) {
|
||||||
|
fallbackCategory =
|
||||||
|
(structuralProduct?.typeProductLabel as string) ||
|
||||||
|
((structuralProduct?.typeProduct as AnyRecord)?.name as string) ||
|
||||||
|
(structuralFamilyCode ? `Famille ${structuralFamilyCode}` : null) ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fallbackSuppliers && structuralProduct?.supplierLabel) {
|
||||||
|
fallbackSuppliers = structuralProduct.supplierLabel as string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fallbackSuppliers && Array.isArray(structuralProduct?.constructeurs)) {
|
||||||
|
const supplierNames = (structuralProduct!.constructeurs as AnyRecord[])
|
||||||
|
.map((c) => c?.name as string)
|
||||||
|
.filter((name) => typeof name === 'string' && name.trim().length > 0)
|
||||||
|
if (supplierNames.length) fallbackSuppliers = supplierNames.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fallbackPrice && structuralProduct?.priceLabel) fallbackPrice = structuralProduct.priceLabel as string
|
||||||
|
if (!fallbackPrice && structuralProduct?.price) fallbackPrice = structuralProduct.price as string | number
|
||||||
|
|
||||||
|
if (fallbackName || fallbackReference || fallbackCategory || fallbackSuppliers || fallbackPrice) {
|
||||||
|
return {
|
||||||
|
name: fallbackName || 'Produit catalogue',
|
||||||
|
reference: fallbackReference,
|
||||||
|
category: fallbackCategory,
|
||||||
|
suppliers: fallbackSuppliers,
|
||||||
|
price:
|
||||||
|
typeof fallbackPrice === 'number'
|
||||||
|
? `${fallbackPrice.toFixed(2)} €`
|
||||||
|
: (fallbackPrice as string) || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Parent link identifiers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const extractParentLinkIdentifiers = (source: AnyRecord | null | undefined): AnyRecord => {
|
||||||
|
if (!source || typeof source !== 'object') return {}
|
||||||
|
|
||||||
|
const identifiers: AnyRecord = {}
|
||||||
|
|
||||||
|
const idKeys = [
|
||||||
|
'parentRequirementId',
|
||||||
|
'parentComponentRequirementId',
|
||||||
|
'parentPieceRequirementId',
|
||||||
|
'parentMachineComponentRequirementId',
|
||||||
|
'parentMachinePieceRequirementId',
|
||||||
|
'parentLinkId',
|
||||||
|
'parentComponentLinkId',
|
||||||
|
'parentPieceLinkId',
|
||||||
|
'parentComponentId',
|
||||||
|
'parentPieceId',
|
||||||
|
]
|
||||||
|
|
||||||
|
idKeys.forEach((key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||||
|
const value = source[key]
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
identifiers[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const objectKeys = [
|
||||||
|
'parentRequirement',
|
||||||
|
'parentComponentRequirement',
|
||||||
|
'parentPieceRequirement',
|
||||||
|
'parentMachineComponentRequirement',
|
||||||
|
'parentMachinePieceRequirement',
|
||||||
|
]
|
||||||
|
|
||||||
|
objectKeys.forEach((key) => {
|
||||||
|
const value = source[key]
|
||||||
|
if (isPlainObject(value) && value.id !== undefined && value.id !== null && value.id !== '') {
|
||||||
|
const idKey = `${key}Id`
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(identifiers, idKey)) {
|
||||||
|
identifiers[idKey] = value.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return identifiers
|
||||||
|
}
|
||||||
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 ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const [localPart, domain] = value.split('@')
|
const parts = value.split('@')
|
||||||
if (!domain) {
|
const localPart = parts[0] ?? ''
|
||||||
|
const domain = parts[1]
|
||||||
|
if (!domain || !localPart) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ export const maskEmail = (rawValue: string): string => {
|
|||||||
return `${localPart[0] ?? ''}·@${domain}`
|
return `${localPart[0] ?? ''}·@${domain}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = localPart[0]
|
const start = localPart[0] ?? ''
|
||||||
const end = localPart.slice(-1)
|
const end = localPart.slice(-1)
|
||||||
const masked = '·'.repeat(Math.max(0, localPart.length - 2))
|
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",
|
"@iconify-json/lucide": "^1.2.68",
|
||||||
"@nuxt/eslint-config": "^1.9.0",
|
"@nuxt/eslint-config": "^1.9.0",
|
||||||
"@rushstack/eslint-patch": "^1.12.0",
|
"@rushstack/eslint-patch": "^1.12.0",
|
||||||
|
"@types/node": "^25.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||||
"@typescript-eslint/parser": "^8.44.1",
|
"@typescript-eslint/parser": "^8.44.1",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
@@ -92,7 +93,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -2087,7 +2087,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -4054,6 +4053,16 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/parse-path": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
"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==",
|
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.44.1",
|
"@typescript-eslint/scope-manager": "8.44.1",
|
||||||
"@typescript-eslint/types": "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",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
||||||
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.28.4",
|
"@babel/parser": "^7.28.4",
|
||||||
"@vue/compiler-core": "3.5.22",
|
"@vue/compiler-core": "3.5.22",
|
||||||
@@ -5004,7 +5011,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5422,7 +5428,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.3",
|
"baseline-browser-mapping": "^2.8.3",
|
||||||
"caniuse-lite": "^1.0.30001741",
|
"caniuse-lite": "^1.0.30001741",
|
||||||
@@ -6782,7 +6787,6 @@
|
|||||||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -10094,7 +10098,6 @@
|
|||||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-is": "^0.1.3",
|
"deep-is": "^0.1.3",
|
||||||
"fast-levenshtein": "^2.0.6",
|
"fast-levenshtein": "^2.0.6",
|
||||||
@@ -10141,7 +10144,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.87.0.tgz",
|
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.87.0.tgz",
|
||||||
"integrity": "sha512-uc47XrtHwkBoES4HFgwgfH9sqwAtJXgAIBq4fFBMZ4hWmgVZoExyn+L4g4VuaecVKXkz1bvlaHcfwHAJPQb5Gw==",
|
"integrity": "sha512-uc47XrtHwkBoES4HFgwgfH9sqwAtJXgAIBq4fFBMZ4hWmgVZoExyn+L4g4VuaecVKXkz1bvlaHcfwHAJPQb5Gw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "^0.87.0"
|
"@oxc-project/types": "^0.87.0"
|
||||||
},
|
},
|
||||||
@@ -10496,7 +10498,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -10936,7 +10937,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
@@ -12481,7 +12481,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
@@ -12701,7 +12700,6 @@
|
|||||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -12749,6 +12747,13 @@
|
|||||||
"@types/estree": "^1.0.0"
|
"@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": {
|
"node_modules/unenv": {
|
||||||
"version": "2.0.0-rc.21",
|
"version": "2.0.0-rc.21",
|
||||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz",
|
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz",
|
||||||
@@ -13052,7 +13057,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"napi-postinstall": "^0.3.0"
|
"napi-postinstall": "^0.3.0"
|
||||||
},
|
},
|
||||||
@@ -13299,7 +13303,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -13625,7 +13628,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
||||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.22",
|
"@vue/compiler-dom": "3.5.22",
|
||||||
"@vue/compiler-sfc": "3.5.22",
|
"@vue/compiler-sfc": "3.5.22",
|
||||||
@@ -13663,7 +13665,6 @@
|
|||||||
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
|
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
"eslint-scope": "^8.2.0",
|
"eslint-scope": "^8.2.0",
|
||||||
@@ -13687,7 +13688,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||||
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-api": "^6.6.4"
|
"@vue/devtools-api": "^6.6.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@iconify-json/lucide": "^1.2.68",
|
"@iconify-json/lucide": "^1.2.68",
|
||||||
"@nuxt/eslint-config": "^1.9.0",
|
"@nuxt/eslint-config": "^1.9.0",
|
||||||
"@rushstack/eslint-patch": "^1.12.0",
|
"@rushstack/eslint-patch": "^1.12.0",
|
||||||
|
"@types/node": "^25.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||||
"@typescript-eslint/parser": "^8.44.1",
|
"@typescript-eslint/parser": "^8.44.1",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user