diff --git a/src/components/generic/Blocks/Admin/VideoBlock.jsx b/src/components/generic/Blocks/Admin/VideoBlock.jsx
index 02cbd08..417a50a 100644
--- a/src/components/generic/Blocks/Admin/VideoBlock.jsx
+++ b/src/components/generic/Blocks/Admin/VideoBlock.jsx
@@ -24,11 +24,18 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
// content.url is redacted (null) server-side for S3 assets — see
// redactS3Url() in controllers/admin/assets.controller.js. Re-resolve
// through media.util.js instead of trusting the persisted url/thumbnail.
- const { src, thumbnailUrl, loading } = useAssetPreviewSrc(
- { asset_id: content?.asset_id, storage_provider: content?.storage_provider, file_url: content?.url, thumbnail_url: content?.thumbnail_url },
+ //
+ // thumbnail_url is intentionally NOT passed here: resolveAssetSrc()'s fast
+ // path falls back to thumbnail_url when there's no stream_token, which for
+ // a video asset with a redacted (null) file_url meant `src` resolved to
+ // the poster JPEG instead of the video file, and the browser then refused
+ // to play it ("media resource ... was not suitable"). Omitting it forces
+ // the async mint-token path, which resolves the real video stream src.
+ const { src, loading } = useAssetPreviewSrc(
+ { asset_id: content?.asset_id, storage_provider: content?.storage_provider, file_url: content?.url },
{ scope: "admin" },
);
- const poster = thumbnailUrl ?? content?.thumbnail_url ?? undefined;
+ const poster = content?.thumbnail_url ?? undefined;
const vidRef = useRef(null);
const wrapRef = useRef(null);
@@ -55,6 +62,13 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
const [mediaLoading, setMediaLoading] = useState(true);
const [hoverProgress, setHoverProgress] = useState(null); // { x, time }
const previewVidRef = useRef(null);
+ // Fullscreen-only auto-hide for the controls bar, mirroring the client
+ // player: idle 3s while playing fades the bar out, any mouse movement (or
+ // a pause) brings it back. Outside fullscreen the bar stays put — it's a
+ // normal in-flow row there, not an overlay, so hiding it would just leave
+ // a blank gap.
+ const [controlsVisible, setControlsVisible] = useState(true);
+ const hideTimer = useRef(null);
// Reset player when video changes
useEffect(() => {
@@ -104,6 +118,19 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
// ── Controls ──────────────────────────────────────────────────────────────
+ const resetHideTimer = useCallback(() => {
+ setControlsVisible(true);
+ clearTimeout(hideTimer.current);
+ if (playing && isFullscreen) {
+ hideTimer.current = setTimeout(() => setControlsVisible(false), 3000);
+ }
+ }, [playing, isFullscreen]);
+
+ useEffect(() => {
+ resetHideTimer();
+ return () => clearTimeout(hideTimer.current);
+ }, [playing, isFullscreen, resetHideTimer]);
+
const togglePlay = useCallback(() => {
const v = vidRef.current;
if (!v) return;
@@ -116,7 +143,8 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
const handleAreaClick = useCallback(() => {
wrapRef.current?.focus({ preventScroll: true });
togglePlay();
- }, [togglePlay]);
+ resetHideTimer();
+ }, [togglePlay, resetHideTimer]);
const restart = () => {
const v = vidRef.current;
@@ -284,6 +312,7 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
ref={wrapRef}
tabIndex={0}
onKeyDown={handleKeyDown}
+ onMouseMove={isFullscreen ? resetHideTimer : undefined}
className={`overflow-hidden bg-card outline-none focus-visible:ring-2 focus-visible:ring-ring ${isFullscreen ? "fixed inset-0 z-[100] flex flex-col" : "rounded-lg border"}`}
>
@@ -342,12 +371,20 @@ export function VideoBlock({ content, onUpdate, readOnly = false }) {
)}
- {/* ── Player controls ── */}
-
+ {/* ── Player controls ──
+ Fullscreen: absolutely positioned over the bottom of the video
+ (wrap is `fixed inset-0` there, so it's the containing block)
+ and fades per controlsVisible. Non-fullscreen: normal in-flow
+ row below the video, always visible. */}
+
{/* Progress bar */}
diff --git a/src/components/generic/FileZoomViewer.jsx b/src/components/generic/FileZoomViewer.jsx
index b7fd7bb..5748846 100644
--- a/src/components/generic/FileZoomViewer.jsx
+++ b/src/components/generic/FileZoomViewer.jsx
@@ -3,9 +3,10 @@
* Type : Component (Generic)
* Description : Zoom/pan/fit viewer for image and PDF files. Used by both the
* client task-attachment preview (FilePreview.jsx, via blob:
- * URLs) and the admin asset preview dialog (AssetPreviewDialog.jsx,
- * via direct stream-token URLs) — `src` accepts either, this
- * component doesn't care how the URL was produced.
+ * URLs) and the admin asset view pages (ViewImageAsset.jsx,
+ * ViewDocumentAsset.jsx, via direct stream-token URLs) —
+ * `src` accepts either, this component doesn't care how the
+ * URL was produced.
*
* Supported:
* image/jpeg, image/png →
with scroll-zoom + drag-pan
@@ -214,7 +215,7 @@ const PdfViewer = ({ src, fileName }) => {
pdfjsLib.GlobalWorkerOptions.workerSrc =
new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
- const doc = await pdfjsLib.getDocument(src).promise;
+ const doc = await pdfjsLib.getDocument({ url: src }).promise;
if (cancelled) return;
setPdfDoc(doc);
setNumPages(doc.numPages);
diff --git a/src/contexts/AdminTaskContext.jsx b/src/contexts/AdminTaskContext.jsx
index 01b0bb5..5e699fa 100644
--- a/src/contexts/AdminTaskContext.jsx
+++ b/src/contexts/AdminTaskContext.jsx
@@ -374,6 +374,20 @@ export function AdminTaskProvider({ children }) {
[request]
);
+ // Batches Task + TaskRequirement creation into one request — used by the
+ // Create Task List wizard so queuing N tasks costs one request instead of N.
+ const createTasksBulk = useCallback(
+ (taskListId, tasks) =>
+ request(async () => {
+ const res = await api.post(`${BASE}/${taskListId}/tasks/bulk`, { tasks });
+ const data = res.data?.data ?? null;
+ toast(`${data?.tasks?.length ?? tasks.length} task(s) created.`);
+ warnPreCompleted(data?.warnings);
+ return data?.tasks ?? null;
+ }),
+ [request]
+ );
+
const updateTask = useCallback(
(taskListId, taskId, payload) =>
request(async () => {
@@ -675,7 +689,7 @@ export function AdminTaskProvider({ children }) {
// ── Task actions ──────────────────────────────────────────────────
fetchTasks, fetchTask, fetchTasksFlat, fetchArchivedTasks,
- createTask, updateTask,
+ createTask, createTasksBulk, updateTask,
archiveTask, restoreTask,
bulkArchiveTasks, bulkRestoreTasks,
permanentlyDeleteTask, bulkPermanentlyDeleteTasks,
diff --git a/src/modules/admin/components/assets/AssetPreviewDialog.jsx b/src/modules/admin/components/assets/AssetPreviewDialog.jsx
deleted file mode 100644
index 2add3e9..0000000
--- a/src/modules/admin/components/assets/AssetPreviewDialog.jsx
+++ /dev/null
@@ -1,114 +0,0 @@
-// modules/admin/components/assets/AssetPreviewDialog.jsx
-//
-// Quick-look dialog for the Assets table's "Preview" row action. Images and
-// PDFs render through the shared FileZoomViewer (zoom/pan). Video and audio
-// reuse the existing admin VideoBlock/AudioBlock players (Blocks/Admin/) —
-// same rich, custom-controls-only UI already used on ViewVideoAsset/
-// ViewAudioAsset and in the lesson block editors — in readOnly mode. No
-// Download button here or anywhere else an asset can be previewed —
-// protecting assets means the raw file is never handed to the browser as a
-// downloadable blob, only streamed inline via short-lived token.
-
-import {
- Dialog, DialogContent, DialogHeader, DialogTitle,
-} from "@/components/ui/dialog";
-import { Badge } from "@/components/ui/badge";
-import { Image, Video, Music, FileText, File } from "lucide-react";
-
-import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
-import FileZoomViewer from "@/components/generic/FileZoomViewer";
-import { VideoBlock } from "@/components/generic/Blocks/Admin/VideoBlock";
-import { AudioBlock } from "@/components/generic/Blocks/Admin/AudioBlock";
-import { TranscodeStatusBanner } from "@/components/generic/TranscodeStatusBanner";
-import { formatFileSize } from "@/utils/format.util";
-
-const KIND_ICON = { image: Image, video: Video, audio: Music, document: FileText };
-const NOOP = () => {};
-
-function PreviewBody({ asset }) {
- const isZoomable = asset.file_type === "image" || asset.file_type === "document";
- const { src, loading } = useAssetPreviewSrc(isZoomable ? asset : null, { scope: "admin" });
-
- if (asset.file_type === "video") {
- return (
-
- );
- }
-
- if (asset.file_type === "audio") {
- return (
-
- );
- }
-
- return (
-
- );
-}
-
-export function AssetPreviewDialog({ asset, open, onOpenChange }) {
- if (!asset) return null;
-
- const Icon = KIND_ICON[asset.file_type] ?? File;
- const fileName = asset.display_name ?? asset.original_name;
-
- return (
-
-
-
-
- {fileName}
-
-
-
-
-
- {asset.mime_type && (
-
- {asset.mime_type}
-
- )}
- {formatFileSize(asset.file_size) && (
-
- {formatFileSize(asset.file_size)}
-
- )}
-
-
-
- );
-}
diff --git a/src/modules/admin/components/assets/AssetsTable.jsx b/src/modules/admin/components/assets/AssetsTable.jsx
index c1dca32..b764e38 100644
--- a/src/modules/admin/components/assets/AssetsTable.jsx
+++ b/src/modules/admin/components/assets/AssetsTable.jsx
@@ -9,7 +9,6 @@ import { useAuth } from "@/contexts/AuthContext";
import DataTable from "@/components/generic/Table/DataTable";
import { FilterSheet } from "@/components/generic/Sheet/FilterSheet";
import { ArchiveDialog } from "@/components/generic/Dialogs/ArchiveDialog";
-import { AssetPreviewDialog } from "./AssetPreviewDialog";
import { buildDataColumns, columnPinning } from "../../config/assets/columns.config";
import { buildToolbarActions } from "../../config/assets/toolbar.config";
@@ -22,7 +21,6 @@ import { formatGeneratedBy } from "@/utils/generatedBy.util";
export default function AssetsTable() {
const [archiveTarget, setArchiveTarget] = useState(null);
const [archiveIds, setArchiveIds] = useState(null);
- const [previewTarget, setPreviewTarget] = useState(null);
const tableRefsRef = useRef({
getFilters: () => [],
@@ -64,8 +62,6 @@ export default function AssetsTable() {
const rowActions = buildRowActions({
onView: (row) => navigate(resolveViewPath(row)),
- onPreview: (row) => setPreviewTarget(row),
- onEdit: (row) => navigate(`edit/${row.asset_id}`),
onArchive: (row) => setArchiveTarget(row),
});
@@ -147,13 +143,6 @@ export default function AssetsTable() {
loading={loading}
onSuccess={handleArchiveSuccess}
/>
-
- {/* ── Quick-look preview ── */}
-
!v && setPreviewTarget(null)}
- />
>
);
}
\ No newline at end of file
diff --git a/src/modules/admin/components/task_list/TasksTable.jsx b/src/modules/admin/components/task_list/TasksTable.jsx
index f738ce8..f3d7253 100644
--- a/src/modules/admin/components/task_list/TasksTable.jsx
+++ b/src/modules/admin/components/task_list/TasksTable.jsx
@@ -103,6 +103,7 @@ export default function TasksTable({ taskListId }) {
const rowActions = buildRowActions({
navigate,
+ taskListId,
onArchive: (row) => setArchiveTarget(row),
onRestore: (row) => setRestoreTarget(row),
onMoveUp: (row) => handleMove(row, -1),
diff --git a/src/modules/admin/config/assets/rowActions.config.jsx b/src/modules/admin/config/assets/rowActions.config.jsx
index 3fb1b32..5a90569 100644
--- a/src/modules/admin/config/assets/rowActions.config.jsx
+++ b/src/modules/admin/config/assets/rowActions.config.jsx
@@ -1,15 +1,13 @@
// modules/admin/config/assets/rowActions.config.jsx
-import { Eye, ScanEye, Pencil, Archive } from "lucide-react";
+import { Eye, Archive } from "lucide-react";
/**
* @param {Object} deps
* @param {Function} deps.onView (row) → void — navigate to view page
- * @param {Function} deps.onPreview (row) → void — open the quick-look preview dialog
- * @param {Function} deps.onEdit (row) → void — navigate to edit page
* @param {Function} deps.onArchive (row) → void — open archive dialog
*/
-export function buildRowActions({ onView, onPreview, onEdit, onArchive }) {
+export function buildRowActions({ onView, onArchive }) {
return [
{
key: "view",
@@ -17,18 +15,6 @@ export function buildRowActions({ onView, onPreview, onEdit, onArchive }) {
icon: ,
onClick: (row) => onView(row),
},
- {
- key: "preview",
- label: "Preview",
- icon: ,
- onClick: (row) => onPreview(row),
- },
- {
- key: "edit",
- label: "Edit Info",
- icon: ,
- onClick: (row) => onEdit(row),
- },
{
key: "archive",
label: "Archive",
diff --git a/src/modules/admin/config/task_list/task/rowActions.config.jsx b/src/modules/admin/config/task_list/task/rowActions.config.jsx
index 989e25f..ae0efe4 100644
--- a/src/modules/admin/config/task_list/task/rowActions.config.jsx
+++ b/src/modules/admin/config/task_list/task/rowActions.config.jsx
@@ -1,12 +1,12 @@
import { Eye, Archive, ArchiveRestore, Info, NotebookPen, ArrowUp, ArrowDown } from "lucide-react";
-export function buildRowActions({ navigate, onArchive, onRestore, onMoveUp, onMoveDown, showArchived, tasks = [] }) {
+export function buildRowActions({ navigate, taskListId, onArchive, onRestore, onMoveUp, onMoveDown, showArchived, tasks = [] }) {
return [
{
key: "view",
label: "View Info",
icon: ,
- onClick: (row) => navigate(`${row.task_id}/view`),
+ onClick: (row) => navigate(`/admin/taskList/${taskListId}/tasks/${row.task_id}/view`),
},
{
key: "move-up",
diff --git a/src/modules/admin/pages/assets/ViewAudioAsset.jsx b/src/modules/admin/pages/assets/ViewAudioAsset.jsx
index f48b87e..9554d40 100644
--- a/src/modules/admin/pages/assets/ViewAudioAsset.jsx
+++ b/src/modules/admin/pages/assets/ViewAudioAsset.jsx
@@ -1,7 +1,7 @@
// modules/admin/pages/assets/ViewAudioAsset.jsx
import { useParams, useNavigate } from "react-router-dom";
-import { ArrowLeft, Lock, Globe, Music2 } from "lucide-react";
+import { ArrowLeft, Lock, Globe, Music2, Pencil } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
@@ -71,6 +71,10 @@ export default function ViewAudioAsset() {
{a.display_name ?? a.original_name}
{a.mime_type}
+
navigate(`/admin/assets/edit/${a.asset_id}`)}>
+
+ Edit Info
+
diff --git a/src/modules/admin/pages/assets/ViewDocumentAsset.jsx b/src/modules/admin/pages/assets/ViewDocumentAsset.jsx
index 90e6dbe..d33c702 100644
--- a/src/modules/admin/pages/assets/ViewDocumentAsset.jsx
+++ b/src/modules/admin/pages/assets/ViewDocumentAsset.jsx
@@ -1,7 +1,7 @@
// modules/admin/pages/assets/ViewDocumentAsset.jsx
import { useParams, useNavigate } from "react-router-dom";
-import { ArrowLeft, Lock, Globe, FileText } from "lucide-react";
+import { ArrowLeft, Lock, Globe, FileText, Pencil } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
@@ -63,6 +63,10 @@ export default function ViewDocumentAsset() {
{a.display_name ?? a.original_name}
{a.mime_type}
+ navigate(`/admin/assets/edit/${a.asset_id}`)}>
+
+ Edit Info
+
diff --git a/src/modules/admin/pages/assets/ViewImageAsset.jsx b/src/modules/admin/pages/assets/ViewImageAsset.jsx
index 85cbb83..b9e4ef1 100644
--- a/src/modules/admin/pages/assets/ViewImageAsset.jsx
+++ b/src/modules/admin/pages/assets/ViewImageAsset.jsx
@@ -1,7 +1,7 @@
// modules/admin/pages/assets/ViewImageAsset.jsx
import { useParams, useNavigate } from "react-router-dom";
-import { ArrowLeft, Lock, Globe } from "lucide-react";
+import { ArrowLeft, Lock, Globe, Pencil } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
@@ -59,6 +59,10 @@ export default function ViewImageAsset() {
{a.display_name ?? a.original_name}
{a.mime_type}
+ navigate(`/admin/assets/edit/${a.asset_id}`)}>
+
+ Edit Info
+
diff --git a/src/modules/admin/pages/assets/ViewVideoAsset.jsx b/src/modules/admin/pages/assets/ViewVideoAsset.jsx
index f591570..1120d7b 100644
--- a/src/modules/admin/pages/assets/ViewVideoAsset.jsx
+++ b/src/modules/admin/pages/assets/ViewVideoAsset.jsx
@@ -1,7 +1,7 @@
// modules/admin/pages/assets/ViewVideoAsset.jsx
import { useParams, useNavigate } from "react-router-dom";
-import { ArrowLeft, Lock, Globe } from "lucide-react";
+import { ArrowLeft, Lock, Globe, Pencil } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
@@ -57,6 +57,10 @@ export default function ViewVideoAsset() {
{a.display_name ?? a.original_name}
{a.mime_type}
+ navigate(`/admin/assets/edit/${a.asset_id}`)}>
+
+ Edit Info
+
diff --git a/src/modules/admin/pages/task_list/CreateTaskList.jsx b/src/modules/admin/pages/task_list/CreateTaskList.jsx
index 8344846..5a0564b 100644
--- a/src/modules/admin/pages/task_list/CreateTaskList.jsx
+++ b/src/modules/admin/pages/task_list/CreateTaskList.jsx
@@ -36,7 +36,7 @@ function SummaryRow({ label, value }) {
export default function CreateTaskList() {
const navigate = useNavigate();
const {
- createTaskList, assignGroups, createTask,
+ createTaskList, assignGroups, createTasksBulk,
fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat,
loading,
} = useAdminTask();
@@ -97,9 +97,10 @@ export default function CreateTaskList() {
await assignGroups(created.task_list_id, selectedGroupIds);
}
- // Create any queued tasks under the new task list — non-blocking: navigate regardless
- for (const t of queuedTasks) {
- await createTask(created.task_list_id, {
+ // Create any queued tasks under the new task list in one batched request —
+ // non-blocking: navigate regardless
+ if (queuedTasks.length > 0) {
+ await createTasksBulk(created.task_list_id, queuedTasks.map((t) => ({
name: t.name.trim(),
description: t.description?.trim() || null,
deadline: t.deadline || null,
@@ -109,7 +110,7 @@ export default function CreateTaskList() {
delete req.duration_seconds;
return req;
}),
- });
+ })));
}
navigate(`/admin/taskList/${created.task_list_id}/view`);
diff --git a/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx b/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx
index fc2c723..7fb131b 100644
--- a/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx
+++ b/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx
@@ -94,16 +94,16 @@ function BindingChip({ courses = [] }) {
function BindingLine({ courses = [] }) {
if (!courses.length) {
return (
-
+
- Standalone — not attached to any course
+ Standalone — not attached to any course
);
}
return (
-
+
- {courses.map((c) => c.title).join(', ')}
+ {courses.map((c) => c.title).join(', ')}
);
}
diff --git a/src/modules/admin/pages/task_list/task/ViewTask.jsx b/src/modules/admin/pages/task_list/task/ViewTask.jsx
index 31abfc8..053317a 100644
--- a/src/modules/admin/pages/task_list/task/ViewTask.jsx
+++ b/src/modules/admin/pages/task_list/task/ViewTask.jsx
@@ -4,6 +4,7 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useAdminTask } from '@/contexts/AdminTaskContext';
import { useAuth } from '@/contexts/AuthContext';
import { useDateFormat } from '@/hooks/useDateFormat';
+import api from '@/utils/api.util';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -26,6 +27,7 @@ import {
Link2, Upload, BookOpen, BookMarked, FileCheck2,
Info, GitPullRequest, Tag, Globe, Copy, File, Bookmark,
ClipboardCheck, NotebookPen, Users, Paperclip, Check, X,
+ Layers, Files, ListChecks,
} from 'lucide-react';
import { formatDate } from '@/utils/table.util';
@@ -62,7 +64,7 @@ function MetaRow({ icon: Icon, label, children }) {
}
// ─── Requirement card ─────────────────────────────────────────────────────────
-function RequirementCard({ req }) {
+function RequirementCard({ req, courseCounts }) {
const cfg = REQUIREMENT_CONFIG[req.type] ?? {
label: req.type, badgeLabel: req.type, Icon: FileText,
};
@@ -127,18 +129,54 @@ function RequirementCard({ req }) {
{req.reference_label}
)}
+
+ {req.type === 'read_course' && req.reference_id && (
+ <>
+
+ {courseCounts === undefined ? (
+
+ ) : courseCounts ? (
+ {courseCounts.unitCount}
+ ) : (
+ —
+ )}
+
+
+ {courseCounts === undefined ? (
+
+ ) : courseCounts ? (
+ {courseCounts.lessonCount}
+ ) : (
+ —
+ )}
+
+
+ {courseCounts === undefined ? (
+
+ ) : courseCounts ? (
+ {courseCounts.quizCount}
+ ) : (
+ —
+ )}
+
+ >
+ )}
);
}
// ─── Requirements section ─────────────────────────────────────────────────────
-function TaskRequirementsSection({ requirements = [] }) {
+function TaskRequirementsSection({ requirements = [], courseCountsById = {} }) {
const sorted = [...requirements].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return (
{sorted.length > 0 ? (
sorted.map((req) => (
-
+
))
) : (
No other requirements.
@@ -195,6 +233,7 @@ export default function ViewTask() {
const [bulkRestoreIds, setBulkRestoreIds] = useState(null);
const [reviewTarget, setReviewTarget] = useState(null);
const [reviewNote, setReviewNote] = useState('');
+ const [courseCountsById, setCourseCountsById] = useState({});
const tableRefsRef = useRef({
getFilters: () => [], getSort: () => [], resetSelection: () => {}, tableInstance: null,
@@ -204,6 +243,24 @@ export default function ViewTask() {
fetchTask(taskListId, taskId);
}, [taskListId, taskId]);
+ // ── Structure counts for "Read a Course" requirements — how many units,
+ // lessons, and quizzes the referenced course actually contains ──────────
+ useEffect(() => {
+ if (!task) return;
+ const courseReqs = (task.requirements ?? []).filter(
+ (r) => r.type === 'read_course' && r.reference_id
+ );
+ courseReqs.forEach(({ reference_id }) => {
+ api.get(`/admin/courses/uuid/${reference_id}/structure-counts`)
+ .then(({ data }) => {
+ setCourseCountsById((prev) => ({ ...prev, [reference_id]: data?.data ?? null }));
+ })
+ .catch(() => {
+ setCourseCountsById((prev) => ({ ...prev, [reference_id]: null }));
+ });
+ });
+ }, [task?.task_id]);
+
const handleRefsReady = (refs) => { tableRefsRef.current = refs; };
// ── Toggle accepting submissions ─────────────────────────────────────────
@@ -437,7 +494,7 @@ export default function ViewTask() {
Requirements
-
+
diff --git a/src/modules/client/pages/UnitList.jsx b/src/modules/client/pages/UnitList.jsx
index 4f3ba7e..60f437d 100644
--- a/src/modules/client/pages/UnitList.jsx
+++ b/src/modules/client/pages/UnitList.jsx
@@ -1129,7 +1129,7 @@ const UnitList = () => {
{/* ── Main content ── */}
-
+
{selectedCompletion ? (
) : selectedAssessment ? (
diff --git a/src/modules/client/pages/UnitReader.jsx b/src/modules/client/pages/UnitReader.jsx
index 51b4ff2..84a7919 100644
--- a/src/modules/client/pages/UnitReader.jsx
+++ b/src/modules/client/pages/UnitReader.jsx
@@ -573,7 +573,7 @@ const UnitReader = () => {
{/* ── Task-mode banner ─────────────────────────────────────────── */}
{taskCtx?.has_task && (
-
@@ -587,7 +587,7 @@ const UnitReader = () => {
)}
{/* ── Desktop sidebar ── */}
-
+
{
{/* ── Main content ── */}