refactor(front): extract shared utils and rewire pages
This commit is contained in:
@@ -402,20 +402,28 @@ import { formatProductStructurePreview, normalizeProductStructureForSave } from
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import { getModelType } from '~/services/modelTypes'
|
||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||
|
||||
interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
customFieldValueId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
fieldKey,
|
||||
buildCustomFieldInputs,
|
||||
requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
|
||||
saveCustomFieldValues as _saveCustomFieldValues,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
import {
|
||||
documentIcon,
|
||||
formatSize,
|
||||
shouldInlinePdf,
|
||||
documentPreviewSrc,
|
||||
documentThumbnailClass,
|
||||
downloadDocument,
|
||||
} from '~/shared/utils/documentDisplayUtils'
|
||||
import {
|
||||
historyActionLabel,
|
||||
formatHistoryDate,
|
||||
formatHistoryValue,
|
||||
historyDiffEntries as _historyDiffEntries,
|
||||
} from '~/shared/utils/historyDisplayUtils'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -458,75 +466,8 @@ const historyFieldLabels: Record<string, string> = {
|
||||
constructeurIds: 'Fournisseurs',
|
||||
}
|
||||
|
||||
const historyActionLabel = (action: string) => {
|
||||
if (action === 'create') {
|
||||
return 'Création'
|
||||
}
|
||||
if (action === 'delete') {
|
||||
return 'Suppression'
|
||||
}
|
||||
return 'Modification'
|
||||
}
|
||||
|
||||
const historyDateFormatter = new Intl.DateTimeFormat('fr-FR', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
const formatHistoryDate = (value: string) => {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
return historyDateFormatter.format(date)
|
||||
}
|
||||
|
||||
const formatHistoryValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '—'
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return '—'
|
||||
}
|
||||
return value.map((item) => formatHistoryValue(item)).join(', ')
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const maybeRecord = value as Record<string, unknown>
|
||||
const name = typeof maybeRecord.name === 'string' ? maybeRecord.name : null
|
||||
const id = typeof maybeRecord.id === 'string' ? maybeRecord.id : null
|
||||
if (name && id) {
|
||||
return `${name} (#${id})`
|
||||
}
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
if (id) {
|
||||
return `#${id}`
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch (error) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const historyDiffEntries = (entry: ProductHistoryEntry) => {
|
||||
const diff = entry.diff ?? {}
|
||||
return Object.entries(diff).map(([field, change]) => {
|
||||
const label = historyFieldLabels[field] ?? field
|
||||
const fromLabel = formatHistoryValue(change?.from)
|
||||
const toLabel = formatHistoryValue(change?.to)
|
||||
return {
|
||||
field,
|
||||
label,
|
||||
fromLabel,
|
||||
toLabel,
|
||||
}
|
||||
})
|
||||
}
|
||||
const historyDiffEntries = (entry: ProductHistoryEntry) =>
|
||||
_historyDiffEntries(entry, historyFieldLabels)
|
||||
|
||||
const refreshCustomFieldInputs = (
|
||||
structureOverride?: ProductModelStructure | null,
|
||||
@@ -545,15 +486,7 @@ const editionForm = reactive({
|
||||
})
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
customFieldInputs.value.every((field) => {
|
||||
if (!field.required) {
|
||||
return true
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
return field.value === 'true' || field.value === 'false'
|
||||
}
|
||||
return field.value.trim().length > 0
|
||||
}),
|
||||
_requiredCustomFieldsFilled(customFieldInputs.value),
|
||||
)
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
@@ -562,60 +495,6 @@ const canSubmit = computed(() =>
|
||||
|
||||
const structurePreview = computed(() => formatProductStructurePreview(structure.value))
|
||||
|
||||
const documentIcon = (doc: any) =>
|
||||
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
|
||||
|
||||
const formatSize = (size: number | null | undefined) => {
|
||||
if (size === null || size === undefined) {
|
||||
return '—'
|
||||
}
|
||||
if (size === 0) {
|
||||
return '0 B'
|
||||
}
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
||||
const formatted = size / Math.pow(1024, index)
|
||||
return `${formatted.toFixed(1)} ${units[index]}`
|
||||
}
|
||||
|
||||
const PDF_PREVIEW_MAX_BYTES = 5 * 1024 * 1024
|
||||
|
||||
const shouldInlinePdf = (document: any) => {
|
||||
if (!document || !isPdfDocument(document) || !document.path) {
|
||||
return false
|
||||
}
|
||||
if (typeof document.size === 'number' && document.size > PDF_PREVIEW_MAX_BYTES) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const appendPdfViewerParams = (src: string) => {
|
||||
if (!src || src.startsWith('data:')) {
|
||||
return src || ''
|
||||
}
|
||||
if (src.includes('#')) {
|
||||
return `${src}&toolbar=0&navpanes=0`
|
||||
}
|
||||
return `${src}#toolbar=0&navpanes=0`
|
||||
}
|
||||
|
||||
const documentPreviewSrc = (document: any) => {
|
||||
if (!document?.path) {
|
||||
return ''
|
||||
}
|
||||
if (isPdfDocument(document)) {
|
||||
return appendPdfViewerParams(document.path)
|
||||
}
|
||||
return document.path
|
||||
}
|
||||
|
||||
const documentThumbnailClass = (document: any) => {
|
||||
if (shouldInlinePdf(document) || (isImageDocument(document) && document?.path)) {
|
||||
return 'h-24 w-20'
|
||||
}
|
||||
return 'h-16 w-16'
|
||||
}
|
||||
|
||||
const openPreview = (doc: any) => {
|
||||
if (!doc || !canPreviewDocument(doc)) {
|
||||
@@ -630,20 +509,6 @@ const closePreview = () => {
|
||||
previewDocument.value = null
|
||||
}
|
||||
|
||||
const downloadDocument = (doc: any) => {
|
||||
if (!doc?.path) {
|
||||
return
|
||||
}
|
||||
const target = String(doc.path)
|
||||
if (target.startsWith('data:')) {
|
||||
const link = document.createElement('a')
|
||||
link.href = target
|
||||
link.download = doc.filename || doc.name || 'document'
|
||||
link.click()
|
||||
return
|
||||
}
|
||||
window.open(target, '_blank')
|
||||
}
|
||||
|
||||
const loadProduct = async () => {
|
||||
const id = route.params.id
|
||||
@@ -768,86 +633,6 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldValueId || field.customFieldId || `${field.name}-${index}`
|
||||
|
||||
const buildCustomFieldInputs = (
|
||||
productStructure: ProductModelStructure | null,
|
||||
values: any[] | null | undefined,
|
||||
): CustomFieldInput[] => {
|
||||
if (!productStructure || typeof productStructure !== 'object') {
|
||||
return []
|
||||
}
|
||||
const definitions = Array.isArray(productStructure.customFields) ? productStructure.customFields : []
|
||||
const valueList = Array.isArray(values) ? values : []
|
||||
|
||||
const byId = new Map<string, any>()
|
||||
const byName = new Map<string, any>()
|
||||
|
||||
valueList.forEach((entry) => {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return
|
||||
}
|
||||
const fieldId = entry.customField?.id || entry.customFieldId || null
|
||||
if (fieldId) {
|
||||
byId.set(fieldId, entry)
|
||||
}
|
||||
const fieldName = entry.customField?.name || entry.name || entry.key || null
|
||||
if (fieldName) {
|
||||
byName.set(fieldName, entry)
|
||||
}
|
||||
})
|
||||
|
||||
return definitions
|
||||
.map((definition, index) => {
|
||||
const definitionId = definition.customFieldId || definition.id || null
|
||||
const matched = (definitionId ? byId.get(definitionId) : null) || byName.get(definition.name)
|
||||
const type = typeof definition.type === 'string' ? definition.type : 'text'
|
||||
const options = Array.isArray(definition.options) ? definition.options : []
|
||||
const required = !!definition.required
|
||||
const orderIndex = typeof definition.orderIndex === 'number' ? definition.orderIndex : index
|
||||
|
||||
if (!matched) {
|
||||
return {
|
||||
id: definition.id ?? null,
|
||||
name: definition.name,
|
||||
type,
|
||||
required,
|
||||
options,
|
||||
value: '',
|
||||
customFieldId: definition.customFieldId || definition.id || null,
|
||||
customFieldValueId: null,
|
||||
orderIndex,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedValue = matched.value ?? ''
|
||||
return {
|
||||
id: definition.id ?? null,
|
||||
name: definition.name,
|
||||
type,
|
||||
required,
|
||||
options,
|
||||
value: formatDefaultValue(type, resolvedValue),
|
||||
customFieldId: matched.customField?.id || definition.customFieldId || definition.id || null,
|
||||
customFieldValueId: matched.id ?? null,
|
||||
orderIndex,
|
||||
}
|
||||
})
|
||||
.filter((field): field is CustomFieldInput => !!field?.name)
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
const formatDefaultValue = (type: string, value: any): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return String(value === true || String(value).toLowerCase() === 'true')
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const submitEdition = async () => {
|
||||
if (!product.value) {
|
||||
return
|
||||
@@ -875,7 +660,12 @@ const submitEdition = async () => {
|
||||
const result = await updateProduct(product.value.id, payload)
|
||||
if (result.success && result.data?.id) {
|
||||
product.value = result.data
|
||||
const failedFields = await saveCustomFieldValues(result.data.id)
|
||||
const failedFields = await _saveCustomFieldValues(
|
||||
'product',
|
||||
result.data.id,
|
||||
[],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
if (failedFields.length) {
|
||||
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
|
||||
return
|
||||
@@ -890,46 +680,6 @@ const submitEdition = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveCustomFieldValues = async (productId: string) => {
|
||||
const failed: string[] = []
|
||||
for (const field of customFieldInputs.value) {
|
||||
const value = field.value ?? ''
|
||||
if (field.customFieldValueId) {
|
||||
const result = await updateCustomFieldValue(field.customFieldValueId, { value })
|
||||
if (!result.success) {
|
||||
failed.push(field.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const metadata = field.customFieldId
|
||||
? undefined
|
||||
: { customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required }
|
||||
|
||||
const result = await upsertCustomFieldValue(
|
||||
field.customFieldId,
|
||||
'product',
|
||||
productId,
|
||||
String(value ?? ''),
|
||||
metadata,
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
failed.push(field.name)
|
||||
} else {
|
||||
const createdValue = result.data
|
||||
if (createdValue?.id) {
|
||||
field.customFieldValueId = createdValue.id
|
||||
}
|
||||
const resolvedId = createdValue?.customField?.id || field.customFieldId
|
||||
if (resolvedId) {
|
||||
field.customFieldId = resolvedId
|
||||
}
|
||||
}
|
||||
}
|
||||
return failed
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProduct()
|
||||
})
|
||||
|
||||
@@ -249,6 +249,10 @@ import { formatProductStructurePreview, normalizeProductStructureForSave } from
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import type { ProductModelStructure } from '~/shared/types/inventory'
|
||||
import type { ModelType } from '~/services/modelTypes'
|
||||
import {
|
||||
type CustomFieldInput,
|
||||
normalizeCustomFieldInputs as normalizeCustomFieldInputsFromUtils,
|
||||
} from '~/shared/utils/customFieldFormUtils'
|
||||
|
||||
interface ProductCatalogType extends ModelType {
|
||||
structure: ProductModelStructure | null
|
||||
@@ -276,17 +280,6 @@ const creationForm = reactive({
|
||||
const selectedDocuments = ref<File[]>([])
|
||||
const uploadingDocuments = ref(false)
|
||||
|
||||
interface CustomFieldInput {
|
||||
id: string | null
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options: string[]
|
||||
value: string
|
||||
customFieldId: string | null
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
const customFieldInputs = ref<CustomFieldInput[]>([])
|
||||
|
||||
const loadingTypes = computed(() => loadingProductTypes.value)
|
||||
@@ -337,7 +330,7 @@ watch(selectedType, (type) => {
|
||||
if (!creationForm.name) {
|
||||
creationForm.name = type.name
|
||||
}
|
||||
customFieldInputs.value = normalizeCustomFieldInputs(normalizeProductStructureForSave(type.structure))
|
||||
customFieldInputs.value = normalizeCustomFieldInputsFromUtils(normalizeProductStructureForSave(type.structure))
|
||||
})
|
||||
|
||||
const requiredCustomFieldsFilled = computed(() =>
|
||||
@@ -362,49 +355,6 @@ const canSubmit = computed(() => Boolean(
|
||||
const fieldKey = (field: CustomFieldInput, index: number) =>
|
||||
field.customFieldId || field.id || `${field.name}-${index}`
|
||||
|
||||
const normalizeCustomFieldInputs = (structure: ProductModelStructure | null): CustomFieldInput[] => {
|
||||
if (!structure || typeof structure !== 'object') {
|
||||
return []
|
||||
}
|
||||
const fields = Array.isArray(structure.customFields) ? structure.customFields : []
|
||||
return fields
|
||||
.map((field, index) => normalizeCustomField(field, index))
|
||||
.filter((field): field is CustomFieldInput => field !== null)
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
}
|
||||
|
||||
const normalizeCustomField = (rawField: any, fallbackIndex = 0): CustomFieldInput | null => {
|
||||
if (!rawField || typeof rawField !== 'object') {
|
||||
return null
|
||||
}
|
||||
const name = typeof rawField.name === 'string' ? rawField.name.trim() : ''
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
const type = typeof rawField.type === 'string' ? rawField.type : 'text'
|
||||
const required = !!rawField.required
|
||||
const options = Array.isArray(rawField.options)
|
||||
? rawField.options.filter((option: unknown): option is string => typeof option === 'string')
|
||||
: []
|
||||
const defaultSource = rawField.defaultValue ?? rawField.value ?? rawField.default ?? null
|
||||
const value = formatDefaultValue(type, defaultSource)
|
||||
const id = typeof rawField.id === 'string' ? rawField.id : null
|
||||
const customFieldId = typeof rawField.customFieldId === 'string' ? rawField.customFieldId : id
|
||||
const orderIndex = typeof rawField.orderIndex === 'number' ? rawField.orderIndex : fallbackIndex
|
||||
|
||||
return { id, name, type, required, options, value, customFieldId, orderIndex }
|
||||
}
|
||||
|
||||
const formatDefaultValue = (type: string, defaultValue: any): string => {
|
||||
if (defaultValue === null || defaultValue === undefined) {
|
||||
return ''
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return String(defaultValue === true || String(defaultValue).toLowerCase() === 'true')
|
||||
}
|
||||
return String(defaultValue)
|
||||
}
|
||||
|
||||
const clearForm = () => {
|
||||
creationForm.name = ''
|
||||
creationForm.reference = ''
|
||||
|
||||
Reference in New Issue
Block a user