import { ref } from 'vue' /** * State machine + collection manager for a single DRF list endpoint. * * Owns: * - items[], isLoading/isError/errorMessage (fetch state) * - deleteError (separate ref so the delete dialog's banner is not * shared with the edit dialog — fixes the cross-dialog error bug) * - editingItem / editingDialogOpen (merged edit+create) * - itemToDelete / deleteDialogOpen * - applySaved / applyDeleted list mutators * - 5 toggle methods (openEdit / openCreate / openDelete / closeEdit / * closeDelete) * * Does NOT own: * - i18n keys (caller passes already-translated strings) * - form state (the edit dialog owns its own editForm) * - file upload (inline in the dialog or in a sibling sub-component) * - reference data (the caller fetches whatever it needs) * - the success toast (caller wires useFlashMessage) * * @param {{ * list: () => Promise, * create: (data: object) => Promise, * update: (id: any, data: object) => Promise, * deleteFn: (id: any) => Promise, * idKey: string, * mapRow?: (row: object) => object, * loadErrorKey?: string, * }} cfg */ export function useCrudList({ list, // create/update/deleteFn are part of the seam shape but called by the // dialog components themselves (they own the form state). Composable // validates the config by destructuring them — lint-friendly prefix. // eslint-disable-next-line no-unused-vars create: _create, // eslint-disable-next-line no-unused-vars update: _update, // eslint-disable-next-line no-unused-vars deleteFn: _deleteFn, idKey, mapRow, loadErrorKey, }) { // — list & fetch state — const items = ref([]) const isLoading = ref(false) const isError = ref(false) const errorMessage = ref('') let loadTimer = null // — dialog state — const editingItem = ref(null) // null = create, object = edit const editingDialogOpen = ref(false) const itemToDelete = ref(null) const deleteDialogOpen = ref(false) // — delete dialog's own error ref (was previously shared with edit dialog) — const deleteError = ref('') function clearLoadError() { if (loadTimer !== null) { clearTimeout(loadTimer) loadTimer = null } isError.value = false errorMessage.value = '' } async function fetch() { isLoading.value = true clearLoadError() try { const data = await list() const rows = Array.isArray(data) ? data : [] items.value = mapRow ? rows.map(mapRow) : rows } catch (e) { isError.value = true errorMessage.value = e?.response?.data?.detail || e?.message || loadErrorKey || 'Failed to load' loadTimer = setTimeout(clearLoadError, 3000) } finally { isLoading.value = false } } // — 5 toggle methods — function openEdit(item) { editingItem.value = item editingDialogOpen.value = true } function openCreate() { editingItem.value = null editingDialogOpen.value = true } function openDelete(item) { itemToDelete.value = item deleteDialogOpen.value = true } function closeEdit() { editingDialogOpen.value = false editingItem.value = null } function closeDelete() { deleteDialogOpen.value = false itemToDelete.value = null } function clearDeleteError() { deleteError.value = '' } // — list mutators — /** * Merge a backend response into items. If `entity[idKey]` matches an * existing row, splice in place; otherwise prepend. mapRow is invoked * on the merged row so denormalized columns (e.g. TermView's * `student_name` join) stay current after a save. * * @param {object} entity full backend response * @returns {boolean} true if a new row was created, false if updated */ function applySaved(entity) { if (!entity || entity[idKey] == null) return false const idx = items.value.findIndex((r) => r[idKey] === entity[idKey]) const mergedBase = idx === -1 ? entity : { ...items.value[idx], ...entity } const merged = mapRow ? mapRow(mergedBase) : mergedBase if (idx === -1) { items.value.unshift(merged) return true } items.value[idx] = merged return false } function applyDeleted(id) { const idx = items.value.findIndex((r) => r[idKey] === id) if (idx !== -1) items.value.splice(idx, 1) } return { // state items, isLoading, isError, errorMessage, deleteError, editingItem, editingDialogOpen, itemToDelete, deleteDialogOpen, // fetch fetch, clearLoadError, // toggles openEdit, openCreate, openDelete, closeEdit, closeDelete, clearDeleteError, // mutators applySaved, applyDeleted, } }