task list working

This commit is contained in:
rgrgogu
2026-07-16 19:04:02 +08:00
parent ac2ba33e24
commit 5b9f174718
7 changed files with 93 additions and 44 deletions
+2 -2
View File
@@ -332,7 +332,7 @@ export function AdminTaskProvider({ children }) {
request(async () => { request(async () => {
const res = await api.post(`${BASE}/${taskListId}/tasks`, payload); const res = await api.post(`${BASE}/${taskListId}/tasks`, payload);
toast('Task created.'); toast('Task created.');
return res.data?.data?.data ?? null; return res.data?.data ?? null;
}), }),
[request] [request]
); );
@@ -342,7 +342,7 @@ export function AdminTaskProvider({ children }) {
request(async () => { request(async () => {
const res = await api.patch(`${BASE}/${taskListId}/tasks/${taskId}`, payload); const res = await api.patch(`${BASE}/${taskListId}/tasks/${taskId}`, payload);
toast('Task updated.'); toast('Task updated.');
return res.data?.data?.data ?? null; return res.data?.data ?? null;
}), }),
[request] [request]
); );
@@ -1,6 +1,7 @@
// config/columns.config.jsx // config/columns.config.jsx
// Column definitions and pinning config for the Users table. // Column definitions and pinning config for the Archived Tasks table.
import { format } from "date-fns";
import { buildColumns } from "@/utils/table.util"; import { buildColumns } from "@/utils/table.util";
import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn"; import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn";
import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn"; import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn";
@@ -10,8 +11,21 @@ export const columnPinning = {
left: [], left: [],
}; };
// ─── Custom cell overrides ────────────────────────────────────────────────────
const cellOverrides = {
deadline: (info) => {
const value = info.getValue();
if (!value) return <span className="text-muted-foreground/40">-</span>;
return (
<span className="text-xs text-muted-foreground">
{format(new Date(value), "MMMM d, yyyy: h:mma")}
</span>
);
},
};
/** /**
* Builds the full column array for the Users table. * Builds the full column array for the Archived Tasks table.
* *
* @param {Array} attributes Field definitions from the server (drives data columns) * @param {Array} attributes Field definitions from the server (drives data columns)
* @param {Array} rowActions Row-level kebab action definitions * @param {Array} rowActions Row-level kebab action definitions
@@ -22,7 +36,7 @@ export function buildDataColumns(attributes, rowActions) {
return [ return [
buildSelectionColumn(), buildSelectionColumn(),
...buildColumns(visibleAttributes), ...buildColumns(visibleAttributes, { cellOverrides }),
buildRowActionsColumn(rowActions, { dropdownLabel: "User Actions" }), buildRowActionsColumn(rowActions, { dropdownLabel: "Task Actions" }),
]; ];
} }
@@ -1,6 +1,7 @@
// config/columns.config.jsx // config/columns.config.jsx
// Column definitions and pinning config for the Users table. // Column definitions and pinning config for the Tasks table.
import { format } from "date-fns";
import { buildColumns } from "@/utils/table.util"; import { buildColumns } from "@/utils/table.util";
import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn"; import { buildSelectionColumn } from "@/components/generic/Table/buildSelectionColumn";
import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn"; import { buildRowActionsColumn } from "@/components/generic/Table/buildRowActionsColumn";
@@ -10,8 +11,21 @@ export const columnPinning = {
left: [], left: [],
}; };
// ─── Custom cell overrides ────────────────────────────────────────────────────
const cellOverrides = {
deadline: (info) => {
const value = info.getValue();
if (!value) return <span className="text-muted-foreground/40">-</span>;
return (
<span className="text-xs text-muted-foreground">
{format(new Date(value), "MMMM d, yyyy: h:mma")}
</span>
);
},
};
/** /**
* Builds the full column array for the Users table. * Builds the full column array for the Tasks table.
* *
* @param {Array} attributes Field definitions from the server (drives data columns) * @param {Array} attributes Field definitions from the server (drives data columns)
* @param {Array} rowActions Row-level kebab action definitions * @param {Array} rowActions Row-level kebab action definitions
@@ -22,7 +36,7 @@ export function buildDataColumns(attributes, rowActions) {
return [ return [
buildSelectionColumn(), buildSelectionColumn(),
...buildColumns(visibleAttributes), ...buildColumns(visibleAttributes, { cellOverrides }),
buildRowActionsColumn(rowActions, { dropdownLabel: "User Actions" }), buildRowActionsColumn(rowActions, { dropdownLabel: "Task Actions" }),
]; ];
} }
@@ -22,6 +22,7 @@ export function buildRowActions({ navigate, onArchive, onRestore, onMoveUp, onMo
icon: <ArrowUp className="size-4" />, icon: <ArrowUp className="size-4" />,
onClick: (row) => onMoveUp(row), onClick: (row) => onMoveUp(row),
hidden: () => showArchived, hidden: () => showArchived,
separator: true,
disabled: (row) => tasks.findIndex((t) => t.task_id === row.task_id) <= 0, disabled: (row) => tasks.findIndex((t) => t.task_id === row.task_id) <= 0,
}, },
{ {
@@ -30,7 +31,6 @@ export function buildRowActions({ navigate, onArchive, onRestore, onMoveUp, onMo
icon: <ArrowDown className="size-4" />, icon: <ArrowDown className="size-4" />,
onClick: (row) => onMoveDown(row), onClick: (row) => onMoveDown(row),
hidden: () => showArchived, hidden: () => showArchived,
separator: true,
disabled: (row) => tasks.findIndex((t) => t.task_id === row.task_id) >= tasks.length - 1, disabled: (row) => tasks.findIndex((t) => t.task_id === row.task_id) >= tasks.length - 1,
}, },
{ {
@@ -14,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import DeadlinePicker from '@/components/generic/DeadlinePicker';
import { import {
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialog, AlertDialogAction, AlertDialogCancel,
AlertDialogContent, AlertDialogDescription, AlertDialogContent, AlertDialogDescription,
@@ -51,9 +52,7 @@ export default function EditTask() {
setForm({ setForm({
name: data.name ?? '', name: data.name ?? '',
description: data.description ?? '', description: data.description ?? '',
deadline: data.deadline deadline: data.deadline ?? '',
? new Date(data.deadline).toISOString().slice(0, 16)
: '',
status: data.status ?? 'pending', status: data.status ?? 'pending',
is_required: data.is_required ?? true, is_required: data.is_required ?? true,
requirements: reqs, requirements: reqs,
@@ -185,32 +184,30 @@ export default function EditTask() {
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="space-y-3">
<div className="space-y-1"> <Label>Deadline</Label>
<Label htmlFor="deadline">Deadline</Label> <DeadlinePicker
<Input value={form.deadline}
id="deadline" onChange={(iso) => setForm({ ...form, deadline: iso })}
type="datetime-local" disabled={loading}
value={form.deadline} />
onChange={(e) => setForm({ ...form, deadline: e.target.value })} </div>
/>
</div> <div className="space-y-1">
<div className="space-y-1"> <Label>Status</Label>
<Label>Status</Label> <Select
<Select value={form.status}
value={form.status} onValueChange={(v) => setForm({ ...form, status: v })}
onValueChange={(v) => setForm({ ...form, status: v })} >
> <SelectTrigger>
<SelectTrigger> <SelectValue />
<SelectValue /> </SelectTrigger>
</SelectTrigger> <SelectContent>
<SelectContent> {STATUS_OPTIONS.map((s) => (
{STATUS_OPTIONS.map((s) => ( <SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem> ))}
))} </SelectContent>
</SelectContent> </Select>
</Select>
</div>
</div> </div>
<div className="flex items-center justify-between rounded-md border px-3 py-2.5"> <div className="flex items-center justify-between rounded-md border px-3 py-2.5">
@@ -12,6 +12,7 @@ import {
ArrowLeft, Pencil, FileText, CalendarClock, ArrowLeft, Pencil, FileText, CalendarClock,
Link2, Upload, BookOpen, BookMarked, FileCheck2, Link2, Upload, BookOpen, BookMarked, FileCheck2,
Info, GitPullRequest, Tag, Globe, Copy, File, Bookmark, Info, GitPullRequest, Tag, Globe, Copy, File, Bookmark,
ClipboardCheck,
} from 'lucide-react'; } from 'lucide-react';
// ─── Same config as ViewTaskList — label, icon only, all styling via shadcn tokens // ─── Same config as ViewTaskList — label, icon only, all styling via shadcn tokens
@@ -128,8 +129,8 @@ function TaskRequirementsSection({ requirements = [] }) {
export default function ViewTask() { export default function ViewTask() {
const navigate = useNavigate(); const navigate = useNavigate();
const { taskListId, taskId } = useParams(); const { taskListId, taskId } = useParams();
const { fetchTask } = useAdminTask(); const { fetchTask, fetchCompletions, completionPagination, completionLoading } = useAdminTask();
const { fmtDate } = useDateFormat(); const { fmtDateTime } = useDateFormat();
const [task, setTask] = useState(null); const [task, setTask] = useState(null);
@@ -138,6 +139,7 @@ export default function ViewTask() {
if (!data) return; if (!data) return;
setTask(data); setTask(data);
}); });
fetchCompletions(taskListId, taskId, { page: 1, limit: 1 });
}, [taskListId, taskId]); }, [taskListId, taskId]);
if (!task) return ( if (!task) return (
@@ -202,7 +204,7 @@ export default function ViewTask() {
<span className="text-sm"> <span className="text-sm">
Deadline:{' '} Deadline:{' '}
<span className="text-foreground font-medium"> <span className="text-foreground font-medium">
{fmtDate(task.deadline)} {fmtDateTime(task.deadline)}
</span> </span>
</span> </span>
</div> </div>
@@ -224,6 +226,28 @@ export default function ViewTask() {
</> </>
)} )}
{/* Completions */}
<Separator />
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<ClipboardCheck className="size-4" /> Completions
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-foreground">
{completionLoading
? '…'
: `${completionPagination.totalRecords} submission${completionPagination.totalRecords === 1 ? '' : 's'}`}
</span>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/admin/taskList/${taskListId}/tasks/${taskId}/completions`)}
>
View
</Button>
</div>
</div>
<Separator /> <Separator />
{/* Requirements */} {/* Requirements */}
@@ -16,7 +16,7 @@ function normalizeLinkUrl(url) {
export const requirementSchema = z.object({ export const requirementSchema = z.object({
type: z.string(), type: z.string(),
reference_id: z.string().optional(), reference_id: z.string().nullable().optional(),
duration_seconds: z.number().optional(), duration_seconds: z.number().optional(),
}).passthrough().superRefine((req, ctx) => { }).passthrough().superRefine((req, ctx) => {
if (!READ_TYPES.includes(req.type)) return; if (!READ_TYPES.includes(req.type)) return;