import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { type StudentRecord, useStudentStore } from '@/stores/students' type StudentPayload = Omit 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 tableHeaders = [ { title: '', key: 'select', sortable: false, nowrap: true }, { title: '學號', key: 'studentId', sortable: true, minWidth: 120, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '姓名', key: 'name', sortable: true, minWidth: 120, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '系所', key: 'department', sortable: true, minWidth: 140, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '年級', key: 'grade', sortable: true, minWidth: 140, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '入學年度', key: 'enrollYear', sortable: true, minWidth: 120, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '已修學分', key: 'credits', sortable: true, minWidth: 120, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '指導老師', key: 'advisor', sortable: true, minWidth: 120, nowrap: true, cellProps: { class: 'px-1' }, }, { title: 'Email', key: 'email', sortable: true, minWidth: 200, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '電話', key: 'phone', sortable: true, minWidth: 150, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '狀態', key: 'status', sortable: true, minWidth: 120, nowrap: true, cellProps: { class: 'px-1' }, }, { title: '操作', key: 'actions', sortable: false, width: 'auto', nowrap: true, cellProps: { class: 'bg-background' }, }, ] as const const TABLE_BOTTOM_GAP = 64 const TABLE_MIN_HEIGHT = 240 export function useEditableStudentGrid() { const studentStore = useStudentStore() const students = computed(() => studentStore.students) const search = ref({ studentId: '', name: '', department: '', }) const isBulkEditEnabled = ref(false) const isSearchVisible = ref(false) const tableContainerRef = ref(null) const tableScrollParentRef = ref(null) const tableResizeObserver = ref(null) const tableHeight = ref(300) const draftRows = ref>({}) const selectedRowIds = ref([]) function toPayload(student: StudentRecord): StudentPayload { 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 rebuildDraftRows() { draftRows.value = Object.fromEntries(students.value.map((item) => [item.id, toPayload(item)])) } function getDraftRow(id: number) { return draftRows.value[id] ?? null } const getSourceRow = (id: number) => students.value.find((item) => item.id === id) || null function isRowDirty(id: number) { const source = getSourceRow(id) const draft = getDraftRow(id) if (!source || !draft) return false return ( source.studentId !== draft.studentId || source.name !== draft.name || source.department !== draft.department || source.grade !== draft.grade || source.enrollYear !== draft.enrollYear || source.credits !== draft.credits || source.advisor !== draft.advisor || source.email !== draft.email || source.phone !== draft.phone || source.status !== draft.status ) } const filteredStudents = computed(() => { const keywordStudentId = search.value.studentId.trim().toLowerCase() const keywordName = search.value.name.trim().toLowerCase() const selectedDepartment = search.value.department return students.value.filter((item) => { if (studentStore.isMarkedAsDeleted(item.id)) return false const matchesStudentId = keywordStudentId ? item.studentId.toLowerCase().includes(keywordStudentId) : true const matchesName = keywordName ? item.name.toLowerCase().includes(keywordName) : true const matchesDepartment = selectedDepartment ? item.department === selectedDepartment : true return matchesStudentId && matchesName && matchesDepartment }) }) const visibleStudentIds = computed(() => filteredStudents.value.map((item) => item.id)) const isAllVisibleSelected = computed( () => visibleStudentIds.value.length > 0 && visibleStudentIds.value.every((id) => selectedRowIds.value.includes(id)) ) const isPartiallyVisibleSelected = computed( () => visibleStudentIds.value.some((id) => selectedRowIds.value.includes(id)) && !isAllVisibleSelected.value ) const hasAnyChange = computed( () => students.value.some((item) => isRowDirty(item.id)) || studentStore.deletedIds.size > 0 ) const hasSelectedRows = computed(() => selectedRowIds.value.length > 0) function toggleSelectAll(checked: boolean | null) { if (isPartiallyVisibleSelected.value) { selectedRowIds.value = selectedRowIds.value.filter( (id) => !visibleStudentIds.value.includes(id) ) return } if (!checked) { selectedRowIds.value = selectedRowIds.value.filter( (id) => !visibleStudentIds.value.includes(id) ) return } selectedRowIds.value = Array.from( new Set([...selectedRowIds.value, ...visibleStudentIds.value]) ) } function toggleSingleRowSelection(id: number, checked: boolean | null) { if (checked) { if (!selectedRowIds.value.includes(id)) { selectedRowIds.value = [...selectedRowIds.value, id] } return } selectedRowIds.value = selectedRowIds.value.filter((selectedId) => selectedId !== id) } function deleteSingleRow(id: number) { selectedRowIds.value = selectedRowIds.value.filter((selectedId) => selectedId !== id) studentStore.markAsDeleted(id) rebuildDraftRows() } function deleteSelectedRows() { if (selectedRowIds.value.length === 0) return for (const id of selectedRowIds.value) { studentStore.markAsDeleted(id) } selectedRowIds.value = [] rebuildDraftRows() } function saveAllRows() { for (const item of students.value) { const draft = getDraftRow(item.id) if (!draft) continue if (!isRowDirty(item.id)) continue studentStore.updateStudent(item.id, draft) } studentStore.commitDeleted() rebuildDraftRows() } function resetAllRows() { studentStore.restoreDeleted() selectedRowIds.value = [] rebuildDraftRows() } function recalculateTableHeight() { const container = tableContainerRef.value if (!container) return const scrollParent = tableScrollParentRef.value || (container.closest('.overflow-auto') as HTMLElement | null) tableScrollParentRef.value = scrollParent const parentBottom = scrollParent ? scrollParent.getBoundingClientRect().bottom : window.innerHeight const top = container.getBoundingClientRect().top const availableHeight = parentBottom - top - TABLE_BOTTOM_GAP tableHeight.value = Math.max(TABLE_MIN_HEIGHT, Math.floor(availableHeight)) } watch(students, () => { const currentIds = new Set(students.value.map((item) => item.id)) selectedRowIds.value = selectedRowIds.value.filter((id) => currentIds.has(id)) }) watch(students, rebuildDraftRows, { immediate: true }) watch([isSearchVisible, isBulkEditEnabled], async () => { await nextTick() requestAnimationFrame(() => { recalculateTableHeight() }) }) onMounted(() => { const container = tableContainerRef.value if (container) { tableScrollParentRef.value = container.closest('.overflow-auto') as HTMLElement | null tableResizeObserver.value = new ResizeObserver(() => { recalculateTableHeight() }) tableResizeObserver.value.observe(container) if (tableScrollParentRef.value) { tableResizeObserver.value.observe(tableScrollParentRef.value) } } requestAnimationFrame(() => { recalculateTableHeight() }) window.addEventListener('resize', recalculateTableHeight) }) onBeforeUnmount(() => { tableResizeObserver.value?.disconnect() window.removeEventListener('resize', recalculateTableHeight) }) return { departments, draftRows, enrollYears, filteredStudents, getDraftRow, gradeOptions, hasAnyChange, hasSelectedRows, isAllVisibleSelected, isBulkEditEnabled, isPartiallyVisibleSelected, isSearchVisible, saveAllRows, search, selectedRowIds, statuses, tableContainerRef, tableHeaders, tableHeight, toggleSelectAll, toggleSingleRowSelection, deleteSelectedRows, deleteSingleRow, resetAllRows, } }