refactor(front): extract shared utils and rewire pages

This commit is contained in:
Matthieu
2026-02-06 17:16:16 +01:00
parent 1fbd1d1b2e
commit 9ee348fff0
36 changed files with 1686 additions and 2194 deletions

View File

@@ -539,26 +539,34 @@ import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/mo
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import type { ComponentModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
import { canPreviewDocument } from '~/utils/documentPreview'
import {
type CustomFieldInput,
fieldKey,
buildCustomFieldInputs,
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
saveCustomFieldValues as _saveCustomFieldValues,
} from '~/shared/utils/customFieldFormUtils'
import {
documentIcon,
formatSize,
shouldInlinePdf,
documentPreviewSrc,
documentThumbnailClass,
downloadDocument,
} from '~/shared/utils/documentDisplayUtils'
import {
historyActionLabel,
formatHistoryDate,
formatHistoryValue,
historyDiffEntries as _historyDiffEntries,
} from '~/shared/utils/historyDisplayUtils'
interface ComponentCatalogType extends ModelType {
structure: ComponentModelStructure | null
customFields?: Array<Record<string, any>>
}
interface CustomFieldInput {
id: string | null
name: string
type: string
required: boolean
options: string[]
value: string
customFieldId: string | null
customFieldValueId: string | null
orderIndex: number
}
const route = useRoute()
const router = useRouter()
const { get } = useApi()
@@ -601,75 +609,8 @@ const historyFieldLabels: Record<string, string> = {
constructeurIds: 'Fournisseurs',
}
const historyActionLabel = (action: string) => {
if (action === 'create') {
return 'Création'
}
if (action === 'delete') {
return 'Suppression'
}
return 'Modification'
}
const historyDateFormatter = new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'medium',
timeStyle: 'short',
})
const formatHistoryDate = (value: string) => {
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return value
}
return historyDateFormatter.format(date)
}
const formatHistoryValue = (value: unknown): string => {
if (value === null || value === undefined || value === '') {
return '—'
}
if (Array.isArray(value)) {
if (value.length === 0) {
return '—'
}
return value.map((item) => formatHistoryValue(item)).join(', ')
}
if (typeof value === 'object') {
const maybeRecord = value as Record<string, unknown>
const name = typeof maybeRecord.name === 'string' ? maybeRecord.name : null
const id = typeof maybeRecord.id === 'string' ? maybeRecord.id : null
if (name && id) {
return `${name} (#${id})`
}
if (name) {
return name
}
if (id) {
return `#${id}`
}
try {
return JSON.stringify(value)
} catch (error) {
return String(value)
}
}
return String(value)
}
const historyDiffEntries = (entry: ComponentHistoryEntry) => {
const diff = entry.diff ?? {}
return Object.entries(diff).map(([field, change]) => {
const label = historyFieldLabels[field] ?? field
const fromLabel = formatHistoryValue(change?.from)
const toLabel = formatHistoryValue(change?.to)
return {
field,
label,
fromLabel,
toLabel,
}
})
}
const historyDiffEntries = (entry: ComponentHistoryEntry) =>
_historyDiffEntries(entry, historyFieldLabels)
const selectedTypeId = ref<string>('')
const editionForm = reactive({
name: '' as string,
@@ -679,49 +620,6 @@ const editionForm = reactive({
})
const customFieldInputs = ref<CustomFieldInput[]>([])
const documentIcon = (doc: any) =>
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
const formatSize = (size: number | null | undefined) => {
if (size === null || size === undefined) {
return '—'
}
if (size === 0) {
return '0 B'
}
const units = ['B', 'KB', 'MB', 'GB']
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
const formatted = size / Math.pow(1024, index)
return `${formatted.toFixed(1)} ${units[index]}`
}
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
const shouldInlinePdf = (document: any) => {
if (!document || !isPdfDocument(document) || !document.path) {
return false
}
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
return false
}
return true
}
const appendPdfViewerParams = (src: string) => {
if (!src || src.startsWith('data:')) {
return src || ''
}
if (src.includes('#')) {
return `${src}&toolbar=0&navpanes=0`
}
return `${src}#toolbar=0&navpanes=0`
}
const documentPreviewSrc = (document: any) => {
if (!document?.path) {
return ''
}
if (isPdfDocument(document)) {
return appendPdfViewerParams(document.path)
}
return document.path
}
const fetchedPieceTypeMap = ref<Record<string, string>>({})
const pieceTypeLabelMap = computed(() => ({
...Object.fromEntries(
@@ -761,12 +659,6 @@ const componentCatalogMap = computed(() =>
.map((item: any) => [String(item.id), item]),
),
)
const documentThumbnailClass = (document: any) => {
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
return 'h-24 w-20'
}
return 'h-16 w-16'
}
const openPreview = (doc: any) => {
if (!doc || !canPreviewDocument(doc)) {
return
@@ -778,20 +670,6 @@ const closePreview = () => {
previewVisible.value = false
previewDocument.value = null
}
const downloadDocument = (doc: any) => {
if (!doc?.path) {
return
}
const target = String(doc.path)
if (target.startsWith('data:')) {
const link = document.createElement('a')
link.href = target
link.download = doc.filename || doc.name || 'document'
link.click()
return
}
window.open(target, '_blank')
}
const removeDocument = async (documentId: string | number | null | undefined) => {
if (!documentId) {
return
@@ -865,15 +743,7 @@ const refreshCustomFieldInputs = (
}
const requiredCustomFieldsFilled = computed(() =>
customFieldInputs.value.every((field) => {
if (!field.required) {
return true
}
if (field.type === 'boolean') {
return field.value === 'true' || field.value === 'false'
}
return toFieldString(field.value).trim() !== ''
}),
_requiredCustomFieldsFilled(customFieldInputs.value),
)
const canSubmit = computed(() => Boolean(
@@ -883,19 +753,6 @@ const canSubmit = computed(() => Boolean(
!saving.value,
))
const toFieldString = (value: unknown): string => {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'string') {
return value
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value)
}
return ''
}
const fetchComponent = async () => {
const id = route.params.id
if (!id || typeof id !== 'string') {
@@ -996,7 +853,15 @@ const submitEdition = async () => {
const result = await updateComposant(component.value.id, payload)
if (result.success) {
const updatedComponent = result.data
await saveCustomFieldValues(updatedComponent)
await _saveCustomFieldValues(
'composant',
updatedComponent.id,
[
updatedComponent?.typeComposant?.customFields,
updatedComponent?.typeMachineComponentRequirement?.typeComposant?.customFields,
],
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
)
await router.push('/component-catalog')
}
} catch (error: any) {
@@ -1006,260 +871,6 @@ const submitEdition = async () => {
}
}
const buildCustomFieldInputs = (
structure: ComponentModelStructure | null,
values: any[] | null,
): CustomFieldInput[] => {
const normalizedStructure = structure ? normalizeStructureForEditor(structure) : null
const definitions = normalizeCustomFieldInputs(normalizedStructure)
const valueList = Array.isArray(values) ? values : []
const mapById = new Map<string, any>()
const mapByName = new Map<string, any>()
valueList.forEach((entry) => {
if (!entry || typeof entry !== 'object') {
return
}
const fieldId = entry.customField?.id || entry.customFieldId || null
if (fieldId) {
mapById.set(fieldId, entry)
}
const fieldName = entry.customField?.name || entry.name || entry.key || null
if (fieldName) {
mapByName.set(fieldName, entry)
}
})
const resolved: CustomFieldInput[] = definitions.map((definition) => {
const definitionId = definition.customFieldId || definition.id || null
const matched = (definitionId ? mapById.get(definitionId) : null) || mapByName.get(definition.name)
if (!matched) {
return {
...definition,
customFieldId: definition.customFieldId || definition.id,
customFieldValueId: null,
orderIndex: definition.orderIndex,
}
}
const resolvedValue = extractStoredCustomFieldValue(matched)
return {
...definition,
customFieldId: matched.customField?.id || definition.customFieldId || definition.id,
customFieldValueId: matched.id ?? null,
value: formatDefaultValue(definition.type, resolvedValue),
orderIndex: Math.min(
definition.orderIndex ?? 0,
typeof matched.customField?.orderIndex === 'number'
? matched.customField.orderIndex
: definition.orderIndex ?? 0,
),
}
}).sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
return resolved
}
const fieldKey = (field: CustomFieldInput, index: number) =>
field.customFieldValueId || field.id || `${field.name}-${index}`
const normalizeCustomFieldInputs = (structure: ComponentModelStructure | null): CustomFieldInput[] => {
if (!structure || typeof structure !== 'object') {
return []
}
const fields = Array.isArray(structure.customFields) ? structure.customFields : []
return fields
.map((field, index) => normalizeCustomField(field, index))
.filter((field): field is CustomFieldInput => field !== null)
.sort((a, b) => a.orderIndex - b.orderIndex)
}
const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
if (!rawField || typeof rawField !== 'object') {
return null
}
const name = resolveFieldName(rawField)
if (!name) {
return null
}
const type = resolveFieldType(rawField)
const required = resolveRequiredFlag(rawField)
const options = resolveOptions(rawField)
const defaultSource = resolveDefaultValue(rawField)
const value = formatDefaultValue(type, defaultSource)
const id = typeof rawField.id === 'string' ? rawField.id : null
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
const customFieldValueId = typeof rawField.customFieldValueId === 'string'
? rawField.customFieldValueId
: null
const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
return { id, name, type, required, options, value, customFieldId, customFieldValueId, orderIndex }
}
const resolveFieldName = (field: any): string => {
if (typeof field?.name === 'string' && field.name.trim()) {
return field.name.trim()
}
if (typeof field?.key === 'string' && field.key.trim()) {
return field.key.trim()
}
if (typeof field?.label === 'string' && field.label.trim()) {
return field.label.trim()
}
return ''
}
const resolveFieldType = (field: any): string => {
const allowed = ['text', 'number', 'select', 'boolean', 'date']
const rawType =
typeof field?.type === 'string'
? field.type
: typeof field?.value?.type === 'string'
? field.value.type
: ''
const value = rawType.toLowerCase()
return allowed.includes(value) ? value : 'text'
}
const resolveDefaultValue = (field: any): any => {
if (!field || typeof field !== 'object') {
return null
}
if (field.defaultValue !== undefined && field.defaultValue !== null) {
return field.defaultValue
}
if (field.value !== undefined && field.value !== null && typeof field.value !== 'object') {
return field.value
}
if (field.default !== undefined && field.default !== null) {
return field.default
}
if (field.value && typeof field.value === 'object') {
if ((field.value as any).defaultValue !== undefined && (field.value as any).defaultValue !== null) {
return (field.value as any).defaultValue
}
if ((field.value as any).value !== undefined && (field.value as any).value !== null && typeof (field.value as any).value !== 'object') {
return (field.value as any).value
}
}
return null
}
const formatDefaultValue = (type: string, defaultValue: any): string => {
if (defaultValue === null || defaultValue === undefined) {
return ''
}
if (typeof defaultValue === 'object') {
if (defaultValue === null) {
return ''
}
if ('defaultValue' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).defaultValue)
}
if ('value' in (defaultValue as Record<string, any>)) {
return formatDefaultValue(type, (defaultValue as Record<string, any>).value)
}
return ''
}
if (type === 'boolean') {
const normalized = String(defaultValue).toLowerCase()
if (normalized === 'true' || normalized === '1') {
return 'true'
}
if (normalized === 'false' || normalized === '0') {
return 'false'
}
return ''
}
return String(defaultValue)
}
const resolveRequiredFlag = (field: any): boolean => {
if (typeof field?.required === 'boolean') {
return field.required
}
const nestedRequired = field?.value?.required
if (typeof nestedRequired === 'boolean') {
return nestedRequired
}
if (typeof nestedRequired === 'string') {
const normalized = nestedRequired.toLowerCase()
return normalized === 'true' || normalized === '1'
}
return false
}
const resolveOptions = (field: any): string[] => {
const sources = [field?.options, field?.value?.options, field?.value?.choices]
for (const source of sources) {
if (Array.isArray(source)) {
const mapped = source
.map((option: unknown) => {
if (option === null || option === undefined) {
return ''
}
if (typeof option === 'string') {
return option.trim()
}
if (typeof option === 'object') {
const record = option || {}
const keys = ['value', 'label', 'name']
for (const key of keys) {
const candidate = record[key]
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate.trim()
}
}
}
const fallback = String(option).trim()
return fallback === '[object Object]' ? '' : fallback
})
.filter((option) => option.length > 0)
if (mapped.length) {
return mapped
}
}
}
return []
}
const extractStoredCustomFieldValue = (entry: any): any => {
if (entry === null || entry === undefined) {
return ''
}
if (typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean') {
return entry
}
if (typeof entry !== 'object') {
return String(entry)
}
const direct = entry.value
if (direct !== undefined && direct !== null) {
if (typeof direct === 'object') {
if (direct === null) {
return ''
}
if ('value' in direct && direct.value !== undefined && direct.value !== null) {
return direct.value
}
if ('defaultValue' in direct && direct.defaultValue !== undefined && direct.defaultValue !== null) {
return direct.defaultValue
}
return ''
}
return direct
}
if (entry.defaultValue !== undefined && entry.defaultValue !== null) {
return entry.defaultValue
}
if (entry.customFieldValue?.value !== undefined && entry.customFieldValue?.value !== null) {
return entry.customFieldValue.value
}
return ''
}
const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
return Array.isArray(structure?.customFields) ? structure.customFields : []
}
@@ -1513,104 +1124,6 @@ const structureSelections = computed(() => {
}
})
const buildCustomFieldMetadata = (field: CustomFieldInput) => ({
customFieldName: field.name,
customFieldType: field.type,
customFieldRequired: field.required,
customFieldOptions: field.options,
})
const shouldPersistField = (field: CustomFieldInput) => {
if (field.type === 'boolean') {
return field.value === 'true' || field.value === 'false'
}
return toFieldString(field.value).trim() !== ''
}
const formatValueForPersistence = (field: CustomFieldInput) => {
if (field.type === 'boolean') {
return field.value === 'true' ? 'true' : 'false'
}
return toFieldString(field.value).trim()
}
const saveCustomFieldValues = async (updatedComponent: any) => {
if (!updatedComponent || !updatedComponent.id) {
return
}
const definitionMap = new Map<string, string>()
const registerDefinitions = (fields: any[]) => {
if (!Array.isArray(fields)) {
return
}
fields.forEach((field) => {
if (!field || typeof field !== 'object') {
return
}
const name = typeof field.name === 'string' ? field.name : null
const id = typeof field.id === 'string' ? field.id : null
if (name && id && !definitionMap.has(name)) {
definitionMap.set(name, id)
}
})
}
registerDefinitions(updatedComponent?.typeComposant?.customFields)
registerDefinitions(updatedComponent?.typeMachineComponentRequirement?.typeComposant?.customFields)
const resolveDefinitionId = (field: CustomFieldInput) => {
if (field.customFieldId) {
return field.customFieldId
}
if (field.id) {
return field.id
}
return definitionMap.get(field.name) ?? null
}
for (const field of customFieldInputs.value) {
if (!shouldPersistField(field)) {
continue
}
const definitionId = resolveDefinitionId(field)
const metadata = definitionId ? undefined : buildCustomFieldMetadata(field)
const value = formatValueForPersistence(field)
if (field.customFieldValueId) {
const result = await updateCustomFieldValue(field.customFieldValueId, { value })
if (!result.success) {
toast.showError(`Impossible de mettre à jour le champ personnalisé "${field.name}"`)
} else if (definitionId && !field.customFieldId) {
field.customFieldId = definitionId
}
continue
}
const result = await upsertCustomFieldValue(
definitionId,
'composant',
updatedComponent.id,
value,
metadata,
)
if (!result.success) {
toast.showError(`Impossible d'enregistrer le champ personnalisé "${field.name}"`)
} else {
const createdValue = result.data
if (createdValue?.id) {
field.customFieldValueId = createdValue.id
}
const resolvedId = createdValue?.customField?.id || definitionId
if (resolvedId) {
field.customFieldId = resolvedId
}
}
}
}
onMounted(async () => {
await Promise.allSettled([
loadComponentTypes(),

View File

@@ -360,6 +360,10 @@ import { useToast } from '~/composables/useToast'
import { useCustomFields } from '~/composables/useCustomFields'
import { useDocuments } from '~/composables/useDocuments'
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
import {
toFieldString,
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
} from '~/shared/utils/customFieldFormUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import type {
ComponentModelPiece,
@@ -747,15 +751,7 @@ const serializeStructureAssignments = (
}
const requiredCustomFieldsFilled = computed(() =>
customFieldInputs.value.every((field) => {
if (!field.required) {
return true
}
if (field.type === 'boolean') {
return field.value === 'true' || field.value === 'false'
}
return toFieldString(field.value).trim() !== ''
}),
_requiredCustomFieldsFilled(customFieldInputs.value),
)
const canSubmit = computed(() => Boolean(
@@ -766,19 +762,6 @@ const canSubmit = computed(() => Boolean(
!submitting.value,
))
const toFieldString = (value: unknown): string => {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'string') {
return value
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value)
}
return ''
}
const getStructureCustomFields = (structure: ComponentModelStructure | null) => {
return Array.isArray(structure?.customFields) ? structure.customFields : []
}