This commit is contained in:
@@ -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');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user