mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-05 15:23:11 +07:00
thực hiện phân quyền admin giáo viên và học sinh xong
This commit is contained in:
@@ -142,6 +142,8 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
const currentPageRef = useRef(1); // mutable copy for collab callbacks
|
||||
const skipRemoteRef = useRef(false); // prevent echo-back when applying remote events
|
||||
const currentUserIdRef = useRef<number | null>(null); // populated from api.me(); used to identify own vs remote objects
|
||||
const [userRole, setUserRole] = useState<"admin" | "teacher" | "student">("student");
|
||||
const userRoleRef = useRef<"admin" | "teacher" | "student">("student");
|
||||
// Buffer for remote events that arrive while renderPageWithAnnotations is in progress
|
||||
const renderingRef = useRef(false);
|
||||
const pendingRemoteEvents = useRef<RemoteEvent[]>([]);
|
||||
@@ -225,6 +227,8 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
useEffect(() => {
|
||||
api.me().then((u) => {
|
||||
currentUserIdRef.current = u.id;
|
||||
setUserRole(u.role);
|
||||
userRoleRef.current = u.role;
|
||||
// Assign deterministic color from preset palette based on user id
|
||||
const assigned = userIdToPresetColor(u.id);
|
||||
setPenColor(assigned);
|
||||
@@ -299,11 +303,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
skipRemoteRef.current = true;
|
||||
skipObjectTracking.current = true;
|
||||
fabric.util.enlivenObjects([objJson], (objects: any[]) => {
|
||||
const canInteract = userRoleRef.current !== "student";
|
||||
objects.forEach((obj: any) => {
|
||||
obj.collab_id = objJson.collab_id;
|
||||
obj._isRemote = true; // don't save other users' live strokes under our account
|
||||
obj.selectable = false;
|
||||
obj.evented = false;
|
||||
obj.selectable = canInteract;
|
||||
obj.evented = canInteract;
|
||||
fc.add(obj);
|
||||
});
|
||||
fc.renderAll();
|
||||
@@ -481,12 +486,13 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
// Mark objects from other users as remote — prevents saving them under current user
|
||||
const uid = currentUserIdRef.current;
|
||||
if (uid !== null) {
|
||||
const canInteract = userRoleRef.current !== "student";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fc.getObjects().forEach((obj: any) => {
|
||||
if (obj._owner_id != null && obj._owner_id !== uid) {
|
||||
obj._isRemote = true;
|
||||
obj.selectable = false;
|
||||
obj.evented = false;
|
||||
obj.selectable = canInteract;
|
||||
obj.evented = canInteract;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -512,11 +518,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
skipRemoteRef.current = true;
|
||||
skipObjectTracking.current = true;
|
||||
fabric.util.enlivenObjects([objJson], (objects: any[]) => {
|
||||
const canInteract = userRoleRef.current !== "student";
|
||||
objects.forEach((obj: any) => {
|
||||
obj.collab_id = objJson.collab_id;
|
||||
obj._isRemote = true;
|
||||
obj.selectable = false;
|
||||
obj.evented = false;
|
||||
obj.selectable = canInteract;
|
||||
obj.evented = canInteract;
|
||||
fc.add(obj);
|
||||
});
|
||||
fc.renderAll();
|
||||
@@ -618,12 +625,13 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
// Mark objects from other users as remote
|
||||
const uid = currentUserIdRef.current;
|
||||
if (uid !== null) {
|
||||
const canInteract = userRoleRef.current !== "student";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fc.getObjects().forEach((obj: any) => {
|
||||
if (obj._owner_id != null && obj._owner_id !== uid) {
|
||||
obj._isRemote = true;
|
||||
obj.selectable = false;
|
||||
obj.evented = false;
|
||||
obj.selectable = canInteract;
|
||||
obj.evented = canInteract;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -899,9 +907,15 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fc.on("mouse:down", (options: any) => {
|
||||
if (!options.target) return;
|
||||
const collab_id = options.target.collab_id as string | undefined;
|
||||
fc.remove(options.target);
|
||||
addedObjects.current = addedObjects.current.filter(o => o !== options.target);
|
||||
const obj = options.target as any;
|
||||
const ownerId: number | undefined = obj._owner_id;
|
||||
const uid = currentUserIdRef.current;
|
||||
const role = userRoleRef.current;
|
||||
// Students can only erase their own annotations
|
||||
if (role === "student" && (obj._isRemote || (ownerId != null && ownerId !== uid))) return;
|
||||
const collab_id = obj.collab_id as string | undefined;
|
||||
fc.remove(obj);
|
||||
addedObjects.current = addedObjects.current.filter(o => o !== obj);
|
||||
fc.renderAll();
|
||||
if (collab_id) collabSendRef.current?.objectRemove(collab_id, currentPageRef.current);
|
||||
});
|
||||
@@ -947,8 +961,14 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
fc.isDrawingMode = false; fc.selection = false;
|
||||
fc.on("mouse:down", (options: any) => {
|
||||
if (!options.target) return;
|
||||
const collab_id = options.target.collab_id as string | undefined;
|
||||
fc.remove(options.target); fc.renderAll();
|
||||
const obj = options.target as any;
|
||||
const ownerId: number | undefined = obj._owner_id;
|
||||
const uid = currentUserIdRef.current;
|
||||
const role = userRoleRef.current;
|
||||
// Students can only erase their own annotations
|
||||
if (role === "student" && (obj._isRemote || (ownerId != null && ownerId !== uid))) return;
|
||||
const collab_id = obj.collab_id as string | undefined;
|
||||
fc.remove(obj); fc.renderAll();
|
||||
if (collab_id) collabSendRef.current?.objectRemove(collab_id, currentPageRef.current);
|
||||
}); break;
|
||||
}
|
||||
@@ -1031,13 +1051,33 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
const handleClear = useCallback(() => {
|
||||
const fc = fabricRef.current;
|
||||
if (!fc) return;
|
||||
if (!confirm("Clear all annotations on this page?")) return;
|
||||
fc.clear();
|
||||
addedObjects.current = [];
|
||||
fc.renderAll();
|
||||
collabSendRef.current?.clear(currentPageRef.current);
|
||||
// Push empty canvas to Redis so other users see the clear immediately on next load
|
||||
syncTempCanvasRef.current(currentPageRef.current);
|
||||
|
||||
const role = userRoleRef.current;
|
||||
const uid = currentUserIdRef.current;
|
||||
|
||||
if (role === "student") {
|
||||
// Students: only remove their own objects, leave others untouched
|
||||
if (!confirm("Xoá annotation của bạn trên trang này?")) return;
|
||||
const toRemove = fc.getObjects().filter((o: any) => !o._isRemote && (o._owner_id == null || o._owner_id === uid));
|
||||
toRemove.forEach((o: any) => {
|
||||
fc.remove(o);
|
||||
// Broadcast removal so teacher canvas updates in real time
|
||||
const collab_id = o.collab_id as string | undefined;
|
||||
if (collab_id) collabSendRef.current?.objectRemove(collab_id, currentPageRef.current);
|
||||
});
|
||||
addedObjects.current = addedObjects.current.filter(o => toRemove.indexOf(o) === -1);
|
||||
fc.renderAll();
|
||||
// Sync own cleared state to Redis
|
||||
syncTempCanvasRef.current(currentPageRef.current);
|
||||
} else {
|
||||
// Teacher / Admin: clear everything
|
||||
if (!confirm("Xoá tất cả annotation trên trang này?")) return;
|
||||
fc.clear();
|
||||
addedObjects.current = [];
|
||||
fc.renderAll();
|
||||
collabSendRef.current?.clear(currentPageRef.current);
|
||||
syncTempCanvasRef.current(currentPageRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -1317,16 +1357,50 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${collabConnected ? "bg-green-500" : "bg-gray-300"}`} />
|
||||
{collabUsers.length > 0 && (
|
||||
<div className="flex -space-x-1">
|
||||
{collabUsers.slice(0, 5).map(u => (
|
||||
<span
|
||||
key={u.user_id}
|
||||
title={u.username}
|
||||
style={{ background: u.color }}
|
||||
className="w-5 h-5 rounded-full border-2 border-white flex items-center justify-center text-[9px] text-white font-bold uppercase"
|
||||
>
|
||||
{u.username[0]}
|
||||
</span>
|
||||
))}
|
||||
{collabUsers.slice(0, 5).map(u => {
|
||||
const isSelf = u.user_id === currentUserIdRef.current;
|
||||
const canClear = !isSelf && (userRole === "admin" || userRole === "teacher");
|
||||
return (
|
||||
<div key={u.user_id} className="relative group">
|
||||
<span
|
||||
title={u.username}
|
||||
style={{ background: u.color }}
|
||||
className="w-5 h-5 rounded-full border-2 border-white flex items-center justify-center text-[9px] text-white font-bold uppercase select-none"
|
||||
>
|
||||
{u.username[0]}
|
||||
</span>
|
||||
{canClear && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm(`Xoá annotation của "${u.username}" trên trang ${currentPageRef.current}?`)) return;
|
||||
try {
|
||||
await api.clearUserAnnotation(pdfId, currentPageRef.current, u.user_id);
|
||||
// Remove their objects from canvas
|
||||
const removeFromCanvas = (fc: any) => {
|
||||
const toRemove = fc.getObjects().filter((o: any) => o._owner_id === u.user_id);
|
||||
toRemove.forEach((o: any) => fc.remove(o));
|
||||
if (toRemove.length) fc.renderAll();
|
||||
};
|
||||
if (scrollModeRef.current === "continuous") {
|
||||
pageFabricRefs.current.forEach(fc => { if (fc) removeFromCanvas(fc); });
|
||||
} else if (fabricRef.current) {
|
||||
removeFromCanvas(fabricRef.current);
|
||||
}
|
||||
localAnnotations.current = {};
|
||||
} catch (e: unknown) {
|
||||
alert(e instanceof Error ? e.message : "Xoá thất bại.");
|
||||
}
|
||||
}}
|
||||
title={`Xoá annotation của ${u.username} trang này`}
|
||||
className="absolute -top-1.5 -right-1.5 w-3.5 h-3.5 rounded-full bg-red-500 text-white hidden group-hover:flex items-center justify-center shadow z-10"
|
||||
style={{ fontSize: 8 }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{collabUsers.length > 5 && (
|
||||
<span className="w-5 h-5 rounded-full border-2 border-white bg-gray-400 flex items-center justify-center text-[9px] text-white font-bold">
|
||||
+{collabUsers.length - 5}
|
||||
|
||||
Reference in New Issue
Block a user