From 24a4ef2c6625c56d447bc68c6dc22c26c22207fe Mon Sep 17 00:00:00 2001 From: PhuocNTB Date: Mon, 13 Jul 2026 11:21:58 +0700 Subject: [PATCH] fixx --- management/src/api.ts | 12 ++- .../src/components/WorkspaceSeatingChart.tsx | 96 +++++++++++++++---- 2 files changed, 84 insertions(+), 24 deletions(-) diff --git a/management/src/api.ts b/management/src/api.ts index 46a7263..ca84e00 100644 --- a/management/src/api.ts +++ b/management/src/api.ts @@ -1223,13 +1223,15 @@ export const apiSeatingLayout = { return json.layoutJson ?? null; }, saveClass: async (classRkId: number, layoutJson: string): Promise => { - await staffFetch(`/classes/${classRkId}/seating-layout`, { + const res = await staffFetch(`/classes/${classRkId}/seating-layout`, { method: 'PUT', body: JSON.stringify({ layoutJson }), }); + if (!res.ok) await parseError(res, 'Lưu sơ đồ lớp thất bại'); }, deleteClass: async (classRkId: number): Promise => { - await staffFetch(`/classes/${classRkId}/seating-layout`, { method: 'DELETE' }); + const res = await staffFetch(`/classes/${classRkId}/seating-layout`, { method: 'DELETE' }); + if (!res.ok) await parseError(res, 'Xóa sơ đồ lớp thất bại'); }, getExam: async (examRoomId: number): Promise => { const res = await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`); @@ -1238,13 +1240,15 @@ export const apiSeatingLayout = { return json.layoutJson ?? null; }, saveExam: async (examRoomId: number, layoutJson: string): Promise => { - await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`, { + const res = await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`, { method: 'PUT', body: JSON.stringify({ layoutJson }), }); + if (!res.ok) await parseError(res, 'Lưu sơ đồ phòng thi thất bại'); }, deleteExam: async (examRoomId: number): Promise => { - await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`, { method: 'DELETE' }); + const res = await staffFetch(`/exam-rooms/${examRoomId}/seating-layout`, { method: 'DELETE' }); + if (!res.ok) await parseError(res, 'Xóa sơ đồ phòng thi thất bại'); }, }; diff --git a/management/src/components/WorkspaceSeatingChart.tsx b/management/src/components/WorkspaceSeatingChart.tsx index 8fc354a..f8f9e99 100644 --- a/management/src/components/WorkspaceSeatingChart.tsx +++ b/management/src/components/WorkspaceSeatingChart.tsx @@ -48,6 +48,7 @@ export const WorkspaceSeatingChart: React.FC = ({ const [editBoardPos, setEditBoardPos] = useState<'top' | 'bottom' | 'left' | 'right'>('top'); const [dragOverCell, setDragOverCell] = useState(null); const [saveNotice, setSaveNotice] = useState(null); + const [saveNoticeKind, setSaveNoticeKind] = useState<'success' | 'error'>('success'); // Toggle layout view modes const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); @@ -67,6 +68,42 @@ export const WorkspaceSeatingChart: React.FC = ({ }, [workspaceId]); const saveTimerRef = useRef | null>(null); + const pendingLayoutRef = useRef(undefined); + const legacyLayoutKey = `sc_seating_layout_${workspaceId}`; + + const showSaveNotice = (message: string, kind: 'success' | 'error' = 'success') => { + setSaveNoticeKind(kind); + setSaveNotice(message); + }; + + const persistLayout = async (newLayout: SeatingLayout | null) => { + if (!workspaceInfo) return; + try { + if (newLayout) { + const json = JSON.stringify(newLayout); + const saver = workspaceInfo.kind === 'class' + ? apiSeatingLayout.saveClass(workspaceInfo.id, json) + : apiSeatingLayout.saveExam(workspaceInfo.id, json); + await saver; + localStorage.removeItem(legacyLayoutKey); + showSaveNotice('Đã lưu sơ đồ chỗ ngồi lên server'); + } else { + const deleter = workspaceInfo.kind === 'class' + ? apiSeatingLayout.deleteClass(workspaceInfo.id) + : apiSeatingLayout.deleteExam(workspaceInfo.id); + await deleter; + localStorage.removeItem(legacyLayoutKey); + showSaveNotice('Đã xóa sơ đồ chỗ ngồi trên server'); + } + } catch { + if (newLayout) { + localStorage.setItem(legacyLayoutKey, JSON.stringify(newLayout)); + } + showSaveNotice('Lưu server thất bại — đã giữ bản cục bộ tạm. Kiểm tra server đã deploy API sơ đồ chưa.', 'error'); + } finally { + pendingLayoutRef.current = undefined; + } + }; // Reverse map seat positions for list view const studentSeats = useMemo(() => { @@ -111,42 +148,60 @@ export const WorkspaceSeatingChart: React.FC = ({ const getter = workspaceInfo.kind === 'class' ? apiSeatingLayout.getClass(workspaceInfo.id) : apiSeatingLayout.getExam(workspaceInfo.id); - getter.then((json) => { + getter.then(async (json) => { if (json) { - try { setLayout(JSON.parse(json)); } catch { setLayout(null); } - } else { + try { + setLayout(JSON.parse(json)); + localStorage.removeItem(legacyLayoutKey); + } catch { + setLayout(null); + } + return; + } + + const legacy = localStorage.getItem(legacyLayoutKey); + if (!legacy) { + setLayout(null); + return; + } + + try { + const parsed = JSON.parse(legacy) as SeatingLayout; + setLayout(parsed); + await persistLayout(parsed); + } catch { setLayout(null); } }).catch(() => setLayout(null)); } else { setLayout(null); } - }, [workspaceId, workspaceInfo]); + }, [workspaceId, workspaceInfo, legacyLayoutKey]); // Save layout — debounce để tránh gọi API quá nhiều khi kéo thả nhanh const saveLayout = (newLayout: SeatingLayout | null) => { setLayout(newLayout); if (!workspaceInfo) return; + pendingLayoutRef.current = newLayout; if (saveTimerRef.current) clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(() => { - if (newLayout) { - const json = JSON.stringify(newLayout); - const saver = workspaceInfo.kind === 'class' - ? apiSeatingLayout.saveClass(workspaceInfo.id, json) - : apiSeatingLayout.saveExam(workspaceInfo.id, json); - saver.catch(() => {}); - setSaveNotice('Đã lưu sơ đồ chỗ ngồi!'); - } else { - const deleter = workspaceInfo.kind === 'class' - ? apiSeatingLayout.deleteClass(workspaceInfo.id) - : apiSeatingLayout.deleteExam(workspaceInfo.id); - deleter.catch(() => {}); - setSaveNotice('Đã xóa sơ đồ chỗ ngồi!'); - } + void persistLayout(newLayout); }, 600); }; + useEffect(() => { + return () => { + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + saveTimerRef.current = null; + } + if (pendingLayoutRef.current !== undefined && workspaceInfo) { + void persistLayout(pendingLayoutRef.current); + } + }; + }, [workspaceInfo, legacyLayoutKey]); + useEffect(() => { if (saveNotice) { const timer = setTimeout(() => { @@ -856,7 +911,7 @@ export const WorkspaceSeatingChart: React.FC = ({ position: 'fixed', bottom: '24px', right: '24px', - background: '#10b981', + background: saveNoticeKind === 'error' ? '#ef4444' : '#10b981', color: '#fff', padding: '0.65rem 1.25rem', borderRadius: '8px', @@ -867,8 +922,9 @@ export const WorkspaceSeatingChart: React.FC = ({ display: 'flex', alignItems: 'center', gap: '8px', + maxWidth: 'min(420px, calc(100vw - 48px))', }}> - {saveNotice} + {saveNoticeKind === 'error' ? '!' : '✓'} {saveNotice} )}