refactor : merge Inventory_frontend submodule into frontend/ directory
Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
327
frontend/app/composables/useMachineCustomFieldDefs.ts
Normal file
327
frontend/app/composables/useMachineCustomFieldDefs.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export type MachineFieldType = 'text' | 'number' | 'select' | 'boolean' | 'date'
|
||||
|
||||
export interface MachineCustomFieldEditorField {
|
||||
uid: string
|
||||
serverId?: string
|
||||
name: string
|
||||
type: MachineFieldType
|
||||
required: boolean
|
||||
optionsText: string
|
||||
orderIndex: number
|
||||
}
|
||||
|
||||
interface InitialDef {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options?: string[]
|
||||
orderIndex: number
|
||||
defaultValue?: unknown
|
||||
}
|
||||
|
||||
interface Deps {
|
||||
machineId: string
|
||||
initialDefs: InitialDef[]
|
||||
onSaved: () => void | Promise<void>
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
let uidCounter = 0
|
||||
const createUid = (): string => {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
uidCounter += 1
|
||||
return `mcf-${Date.now().toString(36)}-${uidCounter}`
|
||||
}
|
||||
|
||||
const normalizeLineEndings = (value: string): string =>
|
||||
value.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||
|
||||
const toEditorField = (def: InitialDef, index: number): MachineCustomFieldEditorField => ({
|
||||
uid: createUid(),
|
||||
serverId: def.id,
|
||||
name: def.name || '',
|
||||
type: (def.type || 'text') as MachineFieldType,
|
||||
required: Boolean(def.required),
|
||||
optionsText: normalizeLineEndings(
|
||||
Array.isArray(def.options) ? def.options.join('\n') : '',
|
||||
),
|
||||
orderIndex: typeof def.orderIndex === 'number' ? def.orderIndex : index,
|
||||
})
|
||||
|
||||
const hydrateFields = (defs: InitialDef[]): MachineCustomFieldEditorField[] =>
|
||||
defs
|
||||
.map((def, index) => toEditorField(def, index))
|
||||
.sort((a, b) => a.orderIndex - b.orderIndex)
|
||||
.map((field, index) => ({ ...field, orderIndex: index }))
|
||||
|
||||
const buildSnapshot = (defs: InitialDef[]): Map<string, InitialDef> => {
|
||||
const map = new Map<string, InitialDef>()
|
||||
for (const def of defs) {
|
||||
map.set(def.id, def)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const applyOrderIndex = (
|
||||
list: MachineCustomFieldEditorField[],
|
||||
): MachineCustomFieldEditorField[] =>
|
||||
list.map((field, index) => ({ ...field, orderIndex: index }))
|
||||
|
||||
const parseOptions = (optionsText: string): string[] =>
|
||||
normalizeLineEndings(optionsText)
|
||||
.split('\n')
|
||||
.map(o => o.trim())
|
||||
.filter(o => o.length > 0)
|
||||
|
||||
// --- Composable ---
|
||||
|
||||
export function useMachineCustomFieldDefs(deps: Deps) {
|
||||
const { apiCall } = useApi()
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
// --- State ---
|
||||
|
||||
const fields = ref<MachineCustomFieldEditorField[]>(hydrateFields(deps.initialDefs))
|
||||
const initialSnapshot = ref<Map<string, InitialDef>>(buildSnapshot(deps.initialDefs))
|
||||
const saving = ref(false)
|
||||
|
||||
// --- CRUD ---
|
||||
|
||||
const addField = () => {
|
||||
const next = fields.value.slice()
|
||||
next.push({
|
||||
uid: createUid(),
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
optionsText: '',
|
||||
orderIndex: next.length,
|
||||
})
|
||||
fields.value = applyOrderIndex(next)
|
||||
}
|
||||
|
||||
const removeField = (index: number) => {
|
||||
const next = fields.value.filter((_, i) => i !== index)
|
||||
fields.value = applyOrderIndex(next)
|
||||
}
|
||||
|
||||
// --- Drag & drop ---
|
||||
|
||||
const dragState = reactive({
|
||||
draggingIndex: null as number | null,
|
||||
dropTargetIndex: null as number | null,
|
||||
})
|
||||
|
||||
const resetDragState = () => {
|
||||
dragState.draggingIndex = null
|
||||
dragState.dropTargetIndex = null
|
||||
}
|
||||
|
||||
const onDragStart = (index: number, event: DragEvent) => {
|
||||
dragState.draggingIndex = index
|
||||
dragState.dropTargetIndex = index
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
const onDragEnter = (index: number) => {
|
||||
if (dragState.draggingIndex === null) return
|
||||
dragState.dropTargetIndex = index
|
||||
}
|
||||
|
||||
const onDrop = (index: number) => {
|
||||
const from = dragState.draggingIndex
|
||||
if (from === null) {
|
||||
resetDragState()
|
||||
return
|
||||
}
|
||||
|
||||
if (from === index) {
|
||||
resetDragState()
|
||||
return
|
||||
}
|
||||
|
||||
const list = fields.value.slice()
|
||||
if (from < 0 || index < 0 || from >= list.length || index >= list.length) {
|
||||
resetDragState()
|
||||
return
|
||||
}
|
||||
|
||||
const [moved] = list.splice(from, 1)
|
||||
if (!moved) {
|
||||
resetDragState()
|
||||
return
|
||||
}
|
||||
list.splice(index, 0, moved)
|
||||
fields.value = applyOrderIndex(list)
|
||||
resetDragState()
|
||||
}
|
||||
|
||||
const onDragEnd = () => {
|
||||
resetDragState()
|
||||
}
|
||||
|
||||
const reorderClass = (index: number): string => {
|
||||
if (dragState.draggingIndex === index) {
|
||||
return 'border-dashed border-primary bg-primary/5'
|
||||
}
|
||||
if (
|
||||
dragState.draggingIndex !== null
|
||||
&& dragState.dropTargetIndex === index
|
||||
&& dragState.draggingIndex !== index
|
||||
) {
|
||||
return 'border-primary border-dashed bg-primary/10'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// --- Save ---
|
||||
|
||||
const saveDefinitions = async () => {
|
||||
if (saving.value) return
|
||||
|
||||
// Validate: remove empty-name fields before saving
|
||||
const emptyNameFields = fields.value.filter(f => !f.name.trim() && !f.serverId)
|
||||
if (emptyNameFields.length > 0) {
|
||||
fields.value = applyOrderIndex(fields.value.filter(f => f.name.trim() || f.serverId))
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const snapshot = initialSnapshot.value
|
||||
const currentServerIds = new Set(
|
||||
fields.value.filter(f => f.serverId).map(f => f.serverId!),
|
||||
)
|
||||
|
||||
// DELETE removed fields
|
||||
const deletedIds = [...snapshot.keys()].filter(id => !currentServerIds.has(id))
|
||||
for (const id of deletedIds) {
|
||||
const result = await apiCall(`/custom_fields/${id}`, { method: 'DELETE' })
|
||||
if (!result.success) {
|
||||
showError('Erreur lors de la suppression d\'un champ personnalisé')
|
||||
await deps.onSaved()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let hasNewFields = false
|
||||
|
||||
for (const field of fields.value) {
|
||||
const name = field.name.trim()
|
||||
if (!name) continue
|
||||
|
||||
const options = field.type === 'select' ? parseOptions(field.optionsText) : []
|
||||
|
||||
if (!field.serverId) {
|
||||
// POST new field
|
||||
hasNewFields = true
|
||||
const body: Record<string, unknown> = {
|
||||
name,
|
||||
type: field.type,
|
||||
required: field.required,
|
||||
options,
|
||||
orderIndex: field.orderIndex,
|
||||
machine: `/api/machines/${deps.machineId}`,
|
||||
}
|
||||
|
||||
const result = await apiCall('/custom_fields', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/ld+json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!result.success) {
|
||||
showError('Erreur lors de la création d\'un champ personnalisé')
|
||||
await deps.onSaved()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// PATCH modified field
|
||||
const original = snapshot.get(field.serverId)
|
||||
const originalOptions = Array.isArray(original?.options)
|
||||
? original.options.join('\n')
|
||||
: ''
|
||||
const currentOptions = field.type === 'select' ? field.optionsText : ''
|
||||
|
||||
const changed
|
||||
= original?.name !== name
|
||||
|| original?.type !== field.type
|
||||
|| original?.required !== field.required
|
||||
|| normalizeLineEndings(originalOptions) !== normalizeLineEndings(currentOptions)
|
||||
|| original?.orderIndex !== field.orderIndex
|
||||
|
||||
if (changed) {
|
||||
const body: Record<string, unknown> = {
|
||||
name,
|
||||
type: field.type,
|
||||
required: field.required,
|
||||
options,
|
||||
orderIndex: field.orderIndex,
|
||||
}
|
||||
|
||||
const result = await apiCall(`/custom_fields/${field.serverId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!result.success) {
|
||||
showError('Erreur lors de la mise à jour d\'un champ personnalisé')
|
||||
await deps.onSaved()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize missing custom field values if new fields were created
|
||||
if (hasNewFields) {
|
||||
await apiCall(`/machines/${deps.machineId}/add-custom-fields`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/ld+json' },
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
}
|
||||
|
||||
showSuccess('Champs personnalisés sauvegardés avec succès')
|
||||
await deps.onSaved()
|
||||
} catch {
|
||||
showError('Erreur inattendue lors de la sauvegarde des champs personnalisés')
|
||||
await deps.onSaved()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reinit ---
|
||||
|
||||
const reinit = (newDefs: InitialDef[]) => {
|
||||
fields.value = hydrateFields(newDefs)
|
||||
initialSnapshot.value = buildSnapshot(newDefs)
|
||||
}
|
||||
|
||||
return {
|
||||
fields,
|
||||
saving,
|
||||
dragState,
|
||||
addField,
|
||||
removeField,
|
||||
onDragStart,
|
||||
onDragEnter,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
reorderClass,
|
||||
saveDefinitions,
|
||||
reinit,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user