fixx
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m14s

This commit is contained in:
2026-07-13 11:21:58 +07:00
parent 120ff1f3c8
commit 24a4ef2c66
2 changed files with 84 additions and 24 deletions

View File

@@ -1223,13 +1223,15 @@ export const apiSeatingLayout = {
return json.layoutJson ?? null;
},
saveClass: async (classRkId: number, layoutJson: string): Promise<void> => {
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<void> => {
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<string | null> => {
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<void> => {
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<void> => {
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');
},
};

View File

@@ -48,6 +48,7 @@ export const WorkspaceSeatingChart: React.FC<WorkspaceSeatingChartProps> = ({
const [editBoardPos, setEditBoardPos] = useState<'top' | 'bottom' | 'left' | 'right'>('top');
const [dragOverCell, setDragOverCell] = useState<string | null>(null);
const [saveNotice, setSaveNotice] = useState<string | null>(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<WorkspaceSeatingChartProps> = ({
}, [workspaceId]);
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingLayoutRef = useRef<SeatingLayout | null | undefined>(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<WorkspaceSeatingChartProps> = ({
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<WorkspaceSeatingChartProps> = ({
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<WorkspaceSeatingChartProps> = ({
display: 'flex',
alignItems: 'center',
gap: '8px',
maxWidth: 'min(420px, calc(100vw - 48px))',
}}>
<span style={{ fontSize: '1.1rem' }}></span> {saveNotice}
<span style={{ fontSize: '1.1rem' }}>{saveNoticeKind === 'error' ? '!' : '✓'}</span> {saveNotice}
</div>
)}
</div>