From 5eefbe0dc9ac0bdc1b4b39d43254eb57968483f4 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Fri, 17 Jul 2026 17:27:59 +0800 Subject: [PATCH] implemented task more --- .../generic/notificationDisplay.jsx | 3 ++ src/data/cronPresets.data.js | 14 ++++- src/modules/admin/pages/jobs/Jobs.jsx | 40 +++++++++++++- .../admin/pages/task_list/ViewTaskList.jsx | 4 +- .../pages/task_list/task/TaskCompletion.jsx | 52 ++++++++++++++----- .../task_list/task/ViewTaskCompletion.jsx | 4 +- .../client/components/TaskListTable.jsx | 4 +- src/modules/client/pages/ViewTask.jsx | 20 ++++--- src/modules/client/pages/ViewTaskDetails.jsx | 4 +- src/utils/table.util.jsx | 6 +++ 10 files changed, 120 insertions(+), 31 deletions(-) diff --git a/src/components/generic/notificationDisplay.jsx b/src/components/generic/notificationDisplay.jsx index aabaa00..25f8630 100644 --- a/src/components/generic/notificationDisplay.jsx +++ b/src/components/generic/notificationDisplay.jsx @@ -115,6 +115,9 @@ export function resolveNotificationLink(type, data) { : null; case "task": + if (data.groupId && data.taskListId && data.taskId) { + return { label: "View task", go: (navigate) => navigate(`/group/${data.groupId}/view/${data.taskListId}/task/${data.taskId}`) }; + } return (data.groupId && data.taskListId) ? { label: "View task list", go: (navigate) => navigate(`/group/${data.groupId}/view/${data.taskListId}`) } : null; diff --git a/src/data/cronPresets.data.js b/src/data/cronPresets.data.js index 3911a5f..7a3e500 100644 --- a/src/data/cronPresets.data.js +++ b/src/data/cronPresets.data.js @@ -17,8 +17,18 @@ export const CRON_PRESET_MAP = Object.fromEntries( // job_name -> friendly copy (label/description come from the API too, but this // is used as a fallback and for the settings icon). export const JOB_LABELS = { - taskOverdue: { label: "Task Overdue Alerts (Admin)", description: "Notifies admins when tasks flip to overdue." }, - userNotifications: { label: "Task Overdue Alerts (Users)", description: "Notifies affected users when their tasks are marked overdue." }, + taskOverdue: { label: "Task Alerts (Admin)", description: "Automatically marks expired tasks as overdue or completed, and notifies admins." }, + userNotifications: { label: "Task Alerts (Users)", description: "Notifies affected users when their tasks are automatically marked overdue or completed." }, issueCertificates: { label: "Certificate Issued", description: "Notifies users when a course certificate is ready." }, expireUserTiers: { label: "Tier Expired", description: "Notifies users when their subscription tier expires." }, }; + +// The only jobs that support a configurable target_status, and the values it accepts. +export const TARGET_STATUS_OPTIONS = [ + { value: "overdue", label: "Overdue" }, + { value: "completed", label: "Completed" }, +]; + +export const TARGET_STATUS_MAP = Object.fromEntries( + TARGET_STATUS_OPTIONS.map((s) => [s.value, s]) +); diff --git a/src/modules/admin/pages/jobs/Jobs.jsx b/src/modules/admin/pages/jobs/Jobs.jsx index 25671de..84bfcb4 100644 --- a/src/modules/admin/pages/jobs/Jobs.jsx +++ b/src/modules/admin/pages/jobs/Jobs.jsx @@ -14,7 +14,7 @@ import { Switch } from "@/components/ui/switch"; import { Spinner } from "@/components/ui/spinner"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { CRON_PRESET_OPTIONS, JOB_LABELS } from "@/data/cronPresets.data"; +import { CRON_PRESET_OPTIONS, JOB_LABELS, TARGET_STATUS_OPTIONS } from "@/data/cronPresets.data"; function SectionCard({ children }) { return
{children}
; @@ -84,6 +84,23 @@ export default function Jobs() { } } + async function handleTargetStatusChange(jobName, target_status) { + setSavingJob(jobName); + try { + const { data } = await api.patch(`/admin/announcement-settings/${jobName}`, { + target_status, + updatedBy: user?.user_id ?? null, + }); + const updated = data?.data?.data; + setSettings((prev) => prev.map((s) => (s.job_name === jobName ? { ...s, ...updated, target_status } : s))); + toast("Target status updated."); + } catch (err) { + toast(err?.response?.data?.message ?? "Failed to update target status."); + } finally { + setSavingJob(null); + } + } + return (
@@ -151,6 +168,27 @@ export default function Jobs() { {isSaving && }
+ + {s.target_status != null && ( +
+ Marks tasks as: + + {isSaving && } +
+ )} ); })} diff --git a/src/modules/admin/pages/task_list/ViewTaskList.jsx b/src/modules/admin/pages/task_list/ViewTaskList.jsx index 1759f99..e31595d 100644 --- a/src/modules/admin/pages/task_list/ViewTaskList.jsx +++ b/src/modules/admin/pages/task_list/ViewTaskList.jsx @@ -137,7 +137,7 @@ export default function ViewTaskList() { const navigate = useNavigate(); const { taskListId } = useParams(); const { fetchTaskList } = useAdminTask(); - const { fmtDate } = useDateFormat(); + const { fmtDateTime } = useDateFormat(); const [taskList, setTaskList] = useState(null); @@ -275,7 +275,7 @@ export default function ViewTaskList() { Deadline:{' '} - {fmtDate(task.deadline)} + {fmtDateTime(task.deadline)} diff --git a/src/modules/admin/pages/task_list/task/TaskCompletion.jsx b/src/modules/admin/pages/task_list/task/TaskCompletion.jsx index b654df7..9d27207 100644 --- a/src/modules/admin/pages/task_list/task/TaskCompletion.jsx +++ b/src/modules/admin/pages/task_list/task/TaskCompletion.jsx @@ -15,13 +15,15 @@ import { RestoreDialog } from '@/components/generic/Dialogs/RestoreDialog'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { House, NotebookPen, Users, Paperclip, Check, X } from 'lucide-react'; import AppBreadcrumb from '@/components/generic/Breadcrumb/AppBreadcrumb'; -import { formatDate } from '@/utils/table.util'; +import { formatDate, formatDateTime } from '@/utils/table.util'; import { getTimestamp } from '@/utils/timestamp.util'; import { buildDataColumns, columnPinning } from '@/modules/admin/config/task_list/task_completion/columns.config'; @@ -51,13 +53,14 @@ export default function TaskCompletions() { task, taskList, completions, completionPagination, setCompletionPagination, completionLoading, completionAttributes, - fetchTask, fetchTaskList, + fetchTask, fetchTaskList, updateTask, fetchCompletions, reviewSubmission, archiveCompletion, restoreCompletion, bulkArchiveCompletions, bulkRestoreCompletions, } = useAdminTask(); const [showArchived, setShowArchived] = useState(false); + const [togglingSubmissions, setTogglingSubmissions] = useState(false); const [archiveTarget, setArchiveTarget] = useState(null); const [restoreTarget, setRestoreTarget] = useState(null); const [bulkArchiveIds, setBulkArchiveIds] = useState(null); @@ -78,6 +81,17 @@ export default function TaskCompletions() { const handleRefsReady = (refs) => { tableRefsRef.current = refs; }; + // ── Toggle accepting submissions ───────────────────────────────────────── + async function handleToggleAcceptingSubmissions(v) { + setTogglingSubmissions(true); + try { + await updateTask(taskListId, taskId, { accepts_submissions: v }); + await fetchTask(taskListId, taskId); + } finally { + setTogglingSubmissions(false); + } + } + // ── Fetch handler ───────────────────────────────────────────────────────── const handleFetch = useCallback((params) => { return fetchCompletions(taskListId, taskId, params); @@ -186,18 +200,28 @@ export default function TaskCompletions() { {/* ── Detail card ───────────────────────────────────────────────── */}
-
- {task - ?

{task.name}

- : - } - {task - ?

- {taskList?.name ?? '—'} - {task.deadline ? ` · Due ${formatDate(task.deadline)}` : ''} -

- : - } +
+
+ {task + ?

{task.name}

+ : + } + {task + ?

+ {taskList?.name ?? '—'} + {task.deadline ? ` · Due ${formatDateTime(task.deadline)}` : ''} +

+ : + } +
+
+ + +
diff --git a/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx b/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx index 2c72fb5..3a8852c 100644 --- a/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx +++ b/src/modules/admin/pages/task_list/task/ViewTaskCompletion.jsx @@ -21,7 +21,7 @@ import { FileVideo, FileAudio, File, ArrowLeft, Clock, CalendarDays, ExternalLink, NotebookPen, } from 'lucide-react'; -import { formatDate } from '@/utils/table.util'; +import { formatDate, formatDateTime } from '@/utils/table.util'; // ─── Helpers ────────────────────────────────────────────────────────────────── const getInitials = (name = '') => @@ -275,7 +275,7 @@ export default function ViewTaskCompletion() { {task?.deadline && ( - Due {formatDate(task.deadline)} + Due {formatDateTime(task.deadline)} )} diff --git a/src/modules/client/components/TaskListTable.jsx b/src/modules/client/components/TaskListTable.jsx index 60cad87..817285c 100644 --- a/src/modules/client/components/TaskListTable.jsx +++ b/src/modules/client/components/TaskListTable.jsx @@ -28,7 +28,7 @@ import { Search, RefreshCw, ChevronsLeft, ChevronLeft, ChevronRight, ChevronsRight, ArrowRight, Check, AlertTriangle, Circle, } from 'lucide-react'; -import { formatDate } from '@/utils/table.util'; +import { formatDateTime } from '@/utils/table.util'; const PAGE_SIZES = [50, 100, 1000]; @@ -168,7 +168,7 @@ export default function ClientTaskListTable({ {tl.description || '—'} - {task?.deadline ? formatDate(task.deadline) : '—'} + {task?.deadline ? formatDateTime(task.deadline) : '—'} diff --git a/src/modules/client/pages/ViewTask.jsx b/src/modules/client/pages/ViewTask.jsx index a30701c..42b936f 100644 --- a/src/modules/client/pages/ViewTask.jsx +++ b/src/modules/client/pages/ViewTask.jsx @@ -35,7 +35,7 @@ import { useTask } from '@/contexts/ClientTaskContext'; import { PageMeta } from '@/contexts/MetadataContext'; import { useTaskProgress } from '@/contexts/ClientTaskProgressContext'; import { useGroup } from '@/contexts/ClientGroupContext'; -import { formatDate } from '@/utils/table.util'; +import { formatDate, formatDateTime } from '@/utils/table.util'; import api from '@/utils/api.util'; // ─── Status badge ───────────────────────────────────────────────────────────── @@ -215,7 +215,7 @@ const FileRow = ({ file, onClick }) => { }; // ─── Submission panel (Your Work) — upload_file and/or submit_text ──────────── -const SubmissionPanel = ({ latestCompletion, onAddAttachment, submitting, onFileClick, requiresReview, hasTextRequirement }) => { +const SubmissionPanel = ({ latestCompletion, onAddAttachment, submitting, onFileClick, requiresReview, hasTextRequirement, acceptsSubmissions }) => { const files = latestCompletion?.files ?? []; return ( @@ -264,9 +264,15 @@ const SubmissionPanel = ({ latestCompletion, onAddAttachment, submitting, onFile ) : null}
- + {acceptsSubmissions ? ( + + ) : ( +

+ This task no longer accepts submissions +

+ )}
); @@ -498,7 +504,7 @@ const ViewTask = () => {
{task?.deadline && ( - Due {formatDate(task.deadline)} + Due {formatDateTime(task.deadline)} )}
@@ -623,6 +629,7 @@ const ViewTask = () => { onFileClick={(file) => setPreviewFile(file)} requiresReview={requiresReview} hasTextRequirement={hasTextReq} + acceptsSubmissions={task?.accepts_submissions !== false} />
)} @@ -638,6 +645,7 @@ const ViewTask = () => { onFileClick={(file) => setPreviewFile(file)} requiresReview={requiresReview} hasTextRequirement={hasTextReq} + acceptsSubmissions={task?.accepts_submissions !== false} /> )} {

- {task.deadline ? `Due ${formatDate(task.deadline)}` : 'No due date'} + {task.deadline ? `Due ${formatDateTime(task.deadline)}` : 'No due date'}
{locked && (

Tap to see what's required to unlock this task.

diff --git a/src/utils/table.util.jsx b/src/utils/table.util.jsx index 037ae10..e1d638c 100644 --- a/src/utils/table.util.jsx +++ b/src/utils/table.util.jsx @@ -98,6 +98,12 @@ export function formatDate(value) { catch { return value; } } +export function formatDateTime(value) { + if (!value) return null; + try { return format(new Date(value), "MMM d, yyyy h:mm a"); } + catch { return value; } +} + // Human-readable labels for boolean-backed enum fields — [falseLabel, trueLabel]. // Shared by renderCell (table cells) and ColumnFilter (filter dropdown), so a // "true"/"false" boolean column never surfaces its raw value to the admin.