import { computed, nextTick, ref, watch } from 'vue' import { useDisplay } from 'vuetify' import { useMaintenanceCrudFlow } from '@/composables/maint/useMaintenanceCrudFlow' import { type StudentFormState, useStudentMaintenanceForm, } from '@/composables/maint/useStudentMaintenanceForm' import type { MaintenancePageModel } from '@/models/page' import { type SemesterRecord, useSemesterStore } from '@/stores/semesters' import { type StudentRecord, useStudentStore } from '@/stores/students' const departments = ['資訊工程', '企業管理', '應用外語', '視覺設計', '財務金融'] const gradeOptions = [ { title: '大一', value: 1 }, { title: '大二', value: 2 }, { title: '大三', value: 3 }, { title: '大四', value: 4 }, { title: '研究所', value: 5 }, ] const enrollYears = [2024, 2023, 2022, 2021, 2020, 2019] const statuses = ['在學', '休學', '畢業'] const itemsPerPage = 10 type StudentPayload = Omit function toFormPayload(student: StudentRecord): StudentFormState { return { studentId: student.studentId, name: student.name, department: student.department, grade: student.grade, enrollYear: student.enrollYear, credits: student.credits, advisor: student.advisor, email: student.email, phone: student.phone, status: student.status, } } function toSavePayload(form: StudentFormState): StudentPayload { return { studentId: form.studentId.trim(), name: form.name.trim(), department: form.department, grade: form.grade, enrollYear: form.enrollYear, credits: form.credits, advisor: form.advisor.trim(), email: form.email.trim(), phone: form.phone.trim(), status: form.status, } } export function useMasterDetailAMaintenancePage() { const studentStore = useStudentStore() const semesterStore = useSemesterStore() const students = computed(() => studentStore.students) const { smAndUp } = useDisplay() const isMobile = computed(() => !smAndUp.value) const pageModel = computed(() => ({ type: 'maintenance', title: '主從資料維護示範A', records: students.value, loading: false, error: null, })) const search = ref({ studentId: '', name: '', department: '', grade: null as number | null, status: '' }) const searchPanelOpen = ref(false) const currentPage = ref(1) const pageCount = computed(() => Math.max(1, Math.ceil(students.value.length / itemsPerPage))) const pageSummary = computed(() => { const total = students.value.length if (total === 0) return '第 0-0 筆 / 共 0 筆' const start = (currentPage.value - 1) * itemsPerPage + 1 const end = Math.min(currentPage.value * itemsPerPage, total) return `第 ${start}-${end} 筆 / 共 ${total} 筆` }) const dialogVisible = ref(false) const editingId = ref(null) const dialogMode = ref<'create' | 'edit' | 'view'>('create') const isLoading = ref(false) const isSaving = ref(false) const snackbarVisible = ref(false) const highlightedId = ref(null) const loadSequence = ref(0) const studentSemesters = ref([]) const selectedSemesterId = ref(null) const activeMobilePanel = ref<'master' | 'detail'>('master') const selectedSemester = computed( () => studentSemesters.value.find((semester) => semester.id === selectedSemesterId.value) || null ) const isDetailEditing = ref(false) const detailForm = ref(null) const formState = useStudentMaintenanceForm({ departments, gradeOptions, enrollYears, statuses, students, editingId, highlightedId, }) const isFormLocked = computed(() => isLoading.value || isSaving.value) const tableHeaders = computed(() => [ { title: '學號', key: 'studentId', sortable: true, fixed: smAndUp.value && ('start' as const), width: 120 }, { title: '姓名', key: 'name', sortable: true, fixed: smAndUp.value && ('start' as const), width: 100 }, { title: '系所', key: 'department', sortable: true, width: 140 }, { title: '年級', key: 'grade', sortable: true, width: 90 }, { title: '入學年度', key: 'enrollYear', sortable: true, width: 110 }, { title: '已修學分', key: 'credits', sortable: true, width: 110 }, { title: 'Email', key: 'email', sortable: true, width: 200 }, { title: '電話', key: 'phone', sortable: true, width: 140 }, { title: '指導老師', key: 'advisor', sortable: true, width: 110 }, { title: '狀態', key: 'status', sortable: true, width: 90 }, { title: '操作', key: 'actions', sortable: false, fixed: smAndUp.value && ('end' as const), width: 'auto', cellProps: { class: 'px-0 bg-background' } }, ]) function resetDetailState() { selectedSemesterId.value = null activeMobilePanel.value = 'master' isDetailEditing.value = false detailForm.value = null } function refreshSemesters() { if (editingId.value) { studentSemesters.value = semesterStore.getStudentSemesters(editingId.value) } } function handleAddSemester() { if (!editingId.value) return const newSemester = semesterStore.addSemester(editingId.value) refreshSemesters() selectedSemesterId.value = newSemester.id activeMobilePanel.value = 'detail' startDetailEdit() } function handleDeleteSemester(id: number) { if (!confirm('確定要刪除此學期紀錄嗎?')) return semesterStore.removeSemester(id) refreshSemesters() if (selectedSemesterId.value === id) { resetDetailState() } } function startDetailEdit() { if (!selectedSemester.value) return detailForm.value = structuredClone(selectedSemester.value) isDetailEditing.value = true } function cancelDetailEdit() { isDetailEditing.value = false detailForm.value = null if (isMobile.value && selectedSemesterId.value === null) { activeMobilePanel.value = 'master' } } function saveDetailEdit() { if (!detailForm.value?.id) return semesterStore.updateSemester(detailForm.value.id, detailForm.value) refreshSemesters() isDetailEditing.value = false detailForm.value = null } function openAddDialog() { loadSequence.value += 1 dialogMode.value = 'create' editingId.value = null studentSemesters.value = [] resetDetailState() formState.resetForm() isLoading.value = false dialogVisible.value = true } function loadRecord(student: StudentRecord, mode: 'edit' | 'view') { loadSequence.value += 1 const sequence = loadSequence.value dialogMode.value = mode editingId.value = student.id studentSemesters.value = semesterStore.getStudentSemesters(student.id) resetDetailState() dialogVisible.value = true isLoading.value = true formState.clearAllErrors() window.setTimeout(() => { if (sequence !== loadSequence.value || !dialogVisible.value) return formState.setForm(toFormPayload(student)) formState.syncInitialForm() isLoading.value = false }, 350) } function openEditDialog(student: StudentRecord) { loadRecord(student, 'edit') } function openViewDialog(student: StudentRecord) { loadRecord(student, 'view') } const flow = useMaintenanceCrudFlow({ records: students, editingId, dialogMode, dialogVisible, isLoading, isSaving, isDirty: formState.isDirty, clearAllErrors: formState.clearAllErrors, resetForm: formState.resetForm, openEditDialog, openViewDialog, removeRecord: (id) => { studentStore.removeStudent(id) semesterStore.removeByStudentId(id) }, describeRecord: (student) => `${student.studentId} ${student.name}`, onCloseReset: resetDetailState, }) const dialogTitle = computed(() => { if (dialogMode.value === 'view') return '檢視主檔資料示範' if (dialogMode.value === 'edit') return '修改主檔資料示範' return '新增主檔資料示範' }) const dialogSubtitle = computed(() => { if (!editingId.value) return '' return `${formState.form.value.studentId || '未填學號'}・${formState.form.value.name || '未填姓名'}` }) watch(pageCount, (value) => { if (currentPage.value > value) currentPage.value = value }) function resetSearch() { search.value = { studentId: '', name: '', department: '', grade: null, status: '' } } async function requestSaveConfirmation() { if (isSaving.value || isLoading.value || !formState.isDirty.value || flow.isViewMode.value) return formState.clearAllErrors() const errors = formState.validateForm() if (errors.length > 0) { for (const error of errors) { formState.fieldErrors.value[error.field] = [error.message] } await nextTick() const firstError = errors[0] if (firstError) scrollToField(firstError.field) return } flow.confirmSaveVisible.value = true } async function confirmSave() { flow.confirmSaveVisible.value = false await saveStudent() } async function saveStudent() { if (isSaving.value || isLoading.value) return isSaving.value = true await new Promise((resolve) => window.setTimeout(resolve, 450)) const payload = toSavePayload(formState.form.value) if (editingId.value) { const updated = studentStore.updateStudent(editingId.value, payload) if (updated) highlightedId.value = editingId.value } else { const createdId = studentStore.addStudent(payload) semesterStore.generateForStudent(createdId) highlightedId.value = createdId } formState.syncInitialForm() dialogVisible.value = false snackbarVisible.value = true isSaving.value = false window.setTimeout(() => { highlightedId.value = null }, 1600) } function scrollToField(field: string) { const target = document.getElementById(`field-${field}`) if (!target) return target.scrollIntoView({ behavior: 'smooth', block: 'center' }) } function handleSemesterSelect(id: number) { if (isMobile.value) { selectedSemesterId.value = id activeMobilePanel.value = 'detail' return } selectedSemesterId.value = selectedSemesterId.value === id ? null : id } function closeDetailPanel() { isDetailEditing.value = false detailForm.value = null if (isMobile.value) { activeMobilePanel.value = 'master' return } selectedSemesterId.value = null } const masterDetailProps = computed(() => ({ activeMobilePanel: activeMobilePanel.value, confirmCloseVisible: flow.confirmCloseVisible.value, confirmDeleteVisible: flow.confirmDeleteVisible.value, confirmNavigateVisible: flow.confirmNavigateVisible.value, confirmSaveVisible: flow.confirmSaveVisible.value, confirmSwitchVisible: flow.confirmSwitchVisible.value, departments, detailForm: detailForm.value, dialogSubtitle: dialogSubtitle.value, dialogTitle: dialogTitle.value, dialogVisible: dialogVisible.value, enrollYears, errorSummary: formState.errorSummary.value, fieldErrors: formState.fieldErrors.value, form: formState.form.value, gradeOptions, hasNextRecord: flow.hasNextRecord.value, hasPrevRecord: flow.hasPrevRecord.value, isDetailEditing: isDetailEditing.value, isDirty: formState.isDirty.value, isEditMode: flow.isEditMode.value, isFormLocked: isFormLocked.value, isFormReadonly: flow.isViewMode.value, isLoading: isLoading.value, isMobile: isMobile.value, isSaving: isSaving.value, isViewMode: flow.isViewMode.value, pendingDeleteLabel: flow.pendingDeleteLabel.value, saveSummary: formState.saveSummary.value, selectedSemester: selectedSemester.value, selectedSemesterId: selectedSemesterId.value, semesters: studentSemesters.value, statuses, })) const masterDetailEvents = { 'add-semester': handleAddSemester, 'cancel-detail-edit': cancelDetailEdit, 'clear-field-error': formState.clearFieldError, close: flow.requestCloseDialog, 'close-detail-panel': closeDetailPanel, 'confirm-close': flow.confirmClose, 'confirm-delete': flow.confirmDelete, 'confirm-navigate': flow.confirmNavigate, 'confirm-save': confirmSave, 'confirm-switch': flow.confirmSwitch, delete: flow.requestDeleteConfirmation, 'delete-current': flow.requestDeleteCurrent, 'delete-semester': handleDeleteSemester, 'dialog-visible-change': flow.handleDialogVisibility, first: () => flow.openEdgeRecord('first'), last: () => flow.openEdgeRecord('last'), next: () => flow.openAdjacentRecord('next'), prev: () => flow.openAdjacentRecord('prev'), save: requestSaveConfirmation, 'save-detail-edit': saveDetailEdit, 'scroll-to-field': scrollToField, 'select-semester': handleSemesterSelect, 'start-detail-edit': startDetailEdit, 'switch-to-edit': flow.switchToEditMode, 'switch-to-view': flow.switchToViewMode, 'update:confirmCloseVisible': (value: boolean) => (flow.confirmCloseVisible.value = value), 'update:confirmDeleteVisible': (value: boolean) => (flow.confirmDeleteVisible.value = value), 'update:confirmNavigateVisible': (value: boolean) => (flow.confirmNavigateVisible.value = value), 'update:confirmSaveVisible': (value: boolean) => (flow.confirmSaveVisible.value = value), 'update:confirmSwitchVisible': (value: boolean) => (flow.confirmSwitchVisible.value = value), 'update:detailForm': (value: SemesterRecord | null) => (detailForm.value = value), 'update:form': (value: StudentFormState) => (formState.form.value = value), } return { currentPage, departments, formState, gradeOptions, itemsPerPage, masterDetailEvents, masterDetailProps, openAddDialog, openEditDialog, openViewDialog, pageCount, pageModel, pageSummary, resetSearch, search, searchPanelOpen, snackbarVisible, statuses, students, tableHeaders, } }