mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -369,6 +369,28 @@ export function AdminTaskProvider({ children }) {
|
||||
[request]
|
||||
);
|
||||
|
||||
// ─── GET TASK LIST FIELD VALUES ───────────────────────────────────────────────
|
||||
const fetchTaskListFieldValues = useCallback(
|
||||
(field) =>
|
||||
request(async () => {
|
||||
const res = await api.get(`${BASE}/field-values`, { params: { field } });
|
||||
return res.data?.data ?? [];
|
||||
}),
|
||||
[request]
|
||||
);
|
||||
|
||||
// ─── GET TASK FIELD VALUES ────────────────────────────────────────────────────
|
||||
const fetchTaskFieldValues = useCallback(
|
||||
(taskListId, field) =>
|
||||
request(async () => {
|
||||
const res = await api.get(`${BASE}/${taskListId}/tasks/field-values`, {
|
||||
params: { field },
|
||||
});
|
||||
return res.data?.data ?? [];
|
||||
}),
|
||||
[request]
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<AdminTaskContext.Provider value={{
|
||||
@@ -383,7 +405,7 @@ export function AdminTaskProvider({ children }) {
|
||||
// task list actions
|
||||
fetchTaskLists, fetchTaskList, fetchArchivedTaskLists,
|
||||
createTaskList, updateTaskList,
|
||||
archiveTaskList, restoreTaskList,
|
||||
archiveTaskList, restoreTaskList, fetchTaskListFieldValues,
|
||||
bulkArchiveTaskLists, bulkRestoreTaskLists,
|
||||
|
||||
// task list group actions
|
||||
@@ -393,7 +415,7 @@ export function AdminTaskProvider({ children }) {
|
||||
|
||||
// task actions
|
||||
fetchTasks, fetchTask, fetchArchivedTasks,
|
||||
createTask, updateTask,
|
||||
createTask, updateTask, fetchTaskFieldValues,
|
||||
archiveTask, restoreTask,
|
||||
bulkArchiveTasks, bulkRestoreTasks,
|
||||
}}>
|
||||
|
||||
@@ -14,12 +14,23 @@ import { buildRowActions } from "@/modules/admin/config/task_list/task/archive/r
|
||||
|
||||
import { getTimestamp } from "@/utils/timestamp.util";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { formatDate } from "@/utils/table.util";
|
||||
import { ListTodo, Users } from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export default function ArchivedTaskTable() {
|
||||
const navigate = useNavigate();
|
||||
const { taskListId } = useParams();
|
||||
|
||||
const {
|
||||
tasks, attributes, pagination, loading,
|
||||
taskList, tasks, attributes, pagination, loading,
|
||||
fetchArchivedTasks,
|
||||
restoreTask,
|
||||
bulkRestoreTasks,
|
||||
@@ -27,11 +38,12 @@ export default function ArchivedTaskTable() {
|
||||
|
||||
const [restoreTarget, setRestoreTarget] = useState(null);
|
||||
const [restoreIds, setRestoreIds] = useState(null);
|
||||
const [showGroupsDialog, setShowGroupsDialog] = useState(false);
|
||||
|
||||
const tableRefsRef = useRef({
|
||||
getFilters: () => [],
|
||||
getSort: () => [],
|
||||
resetSelection: () => {},
|
||||
resetSelection: () => { },
|
||||
tableInstance: null,
|
||||
});
|
||||
|
||||
@@ -91,8 +103,77 @@ export default function ArchivedTaskTable() {
|
||||
[attributes, rowActions]
|
||||
);
|
||||
|
||||
const assignedGroups = taskList?.groups ?? [];
|
||||
const totalTasks = pagination?.totalRecords ?? 0;
|
||||
const hasOverflow = assignedGroups.length > 1;
|
||||
const formattedCreated = taskList?.createdAt ? formatDate(taskList.createdAt) : '—';
|
||||
const formattedUpdated = taskList?.updatedAt ? formatDate(taskList.updatedAt) : '—';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{/* ── Detail card ───────────────────────────────────────────────── */}
|
||||
<div className="bg-card border rounded-xl p-5 flex flex-col gap-4 w-full mb-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
{taskList
|
||||
? <h1 className="text-lg font-medium leading-none">{taskList.name}</h1>
|
||||
: <Skeleton className="h-5 w-48" />
|
||||
}
|
||||
{taskList
|
||||
? <p className="text-sm text-muted-foreground mt-1">{taskList.description ?? '—'}</p>
|
||||
: <Skeleton className="h-4 w-72 mt-1" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="bg-muted rounded-lg px-3 py-2">
|
||||
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground mb-1">Tasks</span>
|
||||
<span className="text-sm font-medium flex items-center gap-1.5">
|
||||
<ListTodo className="size-3.5 text-muted-foreground" />
|
||||
{taskList ? totalTasks : <Skeleton className="h-4 w-8 inline-block" />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted rounded-lg px-3 py-2">
|
||||
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground mb-1">Groups</span>
|
||||
{taskList ? (
|
||||
taskList.group_count > 0 ? (
|
||||
<div className="font-medium flex items-center gap-2">
|
||||
<Users className="size-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm font-medium">{taskList.group_count}</span>
|
||||
{hasOverflow && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGroupsDialog(true)}
|
||||
className="text-xs text-primary hover:underline underline-offset-2 shrink-0"
|
||||
>
|
||||
+{taskList.group_count - 1} more
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : <span className="text-sm font-medium">0</span>
|
||||
) : <Skeleton className="h-4 w-8 inline-block" />}
|
||||
</div>
|
||||
|
||||
<div className="bg-muted rounded-lg px-3 py-2">
|
||||
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground mb-1">Created</span>
|
||||
<span className="text-sm font-medium">
|
||||
{taskList ? formattedCreated : <Skeleton className="h-4 w-20 inline-block" />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted rounded-lg px-3 py-2">
|
||||
<span className="block text-[11px] uppercase tracking-wide text-muted-foreground mb-1">Last Updated</span>
|
||||
<span className="text-sm font-medium">
|
||||
{taskList ? formattedUpdated : <Skeleton className="h-4 w-20 inline-block" />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<DataTable
|
||||
title="Archived Tasks"
|
||||
data={tasks}
|
||||
@@ -142,6 +223,28 @@ export default function ArchivedTaskTable() {
|
||||
loading={loading}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
<Dialog open={showGroupsDialog} onOpenChange={setShowGroupsDialog}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
Assigned Groups
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{assignedGroups.length}
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{assignedGroups.map((g) => (
|
||||
<Badge key={g.group_id} variant="secondary" className="gap-1.5 text-xs py-1 px-2">
|
||||
<Users className="h-3 w-3" />
|
||||
{g.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export default function TaskListTable() {
|
||||
|
||||
const {
|
||||
taskLists, attributes, pagination, loading,
|
||||
fetchTaskLists, fetchArchivedTaskLists,
|
||||
fetchTaskLists, fetchArchivedTaskLists, fetchTaskListFieldValues,
|
||||
archiveTaskList, restoreTaskList,
|
||||
bulkArchiveTaskLists, bulkRestoreTaskLists,
|
||||
} = useAdminTask();
|
||||
@@ -124,7 +124,7 @@ export default function TaskListTable() {
|
||||
pagination={pagination}
|
||||
loading={loading}
|
||||
onFetch={fetchTaskLists}
|
||||
onFetchFilterData={() => []}
|
||||
onFetchFilterData={fetchTaskListFieldValues}
|
||||
onRefsReady={handleRefsReady}
|
||||
renderFilterSheet={({ open, onOpenChange, column, attr, data, loading }) => (
|
||||
<FilterSheet
|
||||
|
||||
@@ -2,12 +2,12 @@ import { RotateCcw, Eye } from "lucide-react";
|
||||
|
||||
export function buildRowActions({ navigate, onRestore }) {
|
||||
return [
|
||||
{
|
||||
key: "view",
|
||||
label: "View",
|
||||
icon: <Eye className="size-4" />,
|
||||
onClick: (row) => navigate(`/admin/taskList/${row.task_list_id}/view`),
|
||||
},
|
||||
// {
|
||||
// key: "view",
|
||||
// label: "View",
|
||||
// icon: <Eye className="size-4" />,
|
||||
// onClick: (row) => navigate(`/admin/taskList/${row.task_list_id}/view`),
|
||||
// },
|
||||
{
|
||||
key: "restore",
|
||||
label: "Restore",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { exportTableToExcel } from "@/utils/excel.util";
|
||||
/**
|
||||
* @param {Object} deps
|
||||
* @param {Function} deps.fetchTasks
|
||||
* @param {string} deps.taskListId
|
||||
* @param {string} deps.taskId
|
||||
* @param {Object} deps.pagination
|
||||
* @param {Object} deps.exportConfig
|
||||
* @param {Function} deps.navigate
|
||||
@@ -17,7 +17,7 @@ import { exportTableToExcel } from "@/utils/excel.util";
|
||||
*/
|
||||
export function buildToolbarActions({
|
||||
fetchTasks,
|
||||
taskListId,
|
||||
taskId,
|
||||
pagination,
|
||||
exportConfig,
|
||||
navigate,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
export default function CreateTaskList() {
|
||||
const navigate = useNavigate();
|
||||
@@ -43,16 +44,23 @@ export default function CreateTaskList() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-xl mx-auto py-8 px-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create Task List</CardTitle>
|
||||
</CardHeader>
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
||||
<div className="mx-auto">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Button type="button" variant="ghost" size="icon" onClick={() => navigate('/admin/taskList')}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Create Task List</h1>
|
||||
<p className="text-sm text-muted-foreground">View course information.</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="lg:w-2xl">
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
|
||||
{/* Name */}
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="name">Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -66,7 +74,7 @@ export default function CreateTaskList() {
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
@@ -78,7 +86,7 @@ export default function CreateTaskList() {
|
||||
</div>
|
||||
|
||||
{/* Groups */}
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-3">
|
||||
<Label>
|
||||
Assign to Groups
|
||||
<span className="ml-1.5 text-xs text-muted-foreground font-normal">
|
||||
@@ -114,5 +122,6 @@ export default function CreateTaskList() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
@@ -100,21 +100,22 @@ export default function EditTaskList() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-xl mx-auto py-8 px-4">
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
||||
<div className="mx-auto">
|
||||
<div className="flex gap-2 items-center mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/admin/taskList')}
|
||||
onClick={() => navigate(`/admin/taskList`)}
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
</Button>
|
||||
<div className="space-y-1">
|
||||
<h1 className="font-medium">Edit Task List</h1>
|
||||
<h1 className="text-xl font-semibold">Edit Task List</h1>
|
||||
<p className="text-sm text-muted-foreground">Update task list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Card className="lg:w-2xl">
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
|
||||
@@ -166,7 +167,7 @@ export default function EditTaskList() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => navigate('/admin/taskList')}
|
||||
onClick={() => navigate(`/admin/taskList`)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -182,5 +183,6 @@ export default function EditTaskList() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -159,7 +159,8 @@ export default function ViewTaskList() {
|
||||
const tasks = taskList.tasks ?? [];
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-8 px-4 space-y-4">
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
||||
<div className="mx-auto space-y-4">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
@@ -193,8 +194,7 @@ export default function ViewTaskList() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Main content — plain div, not Card, to avoid overflow:hidden clipping accordion */}
|
||||
<div className="rounded-lg border border-border bg-card text-card-foreground shadow-sm">
|
||||
<div className="lg:w-2xl rounded-lg border border-border bg-card text-card-foreground shadow-sm">
|
||||
<div className="p-6 space-y-6">
|
||||
|
||||
{/* Assigned Groups */}
|
||||
@@ -230,7 +230,11 @@ export default function ViewTaskList() {
|
||||
</div>
|
||||
|
||||
{tasks.length > 0 ? (
|
||||
<Accordion type="multiple" className="rounded-md border divide-y">
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="rounded-md border divide-y"
|
||||
defaultValue={[String(tasks[0]?.task_id ?? 0)]}
|
||||
>
|
||||
{tasks.map((task, index) => (
|
||||
<AccordionItem
|
||||
key={task.task_id ?? index}
|
||||
@@ -253,7 +257,7 @@ export default function ViewTaskList() {
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="pb-4 pt-0 overflow-visible">
|
||||
<AccordionContent className="pb-4 pt-0">
|
||||
<div className="ml-8 space-y-4 text-sm">
|
||||
|
||||
{task.description && (
|
||||
@@ -294,5 +298,6 @@ export default function ViewTaskList() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,8 @@ export default function CreateTask() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-8 px-4 space-y-6">
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
||||
<div className="mx-auto space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(`/admin/taskList/${taskListId}/tasks`)}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
@@ -60,7 +61,7 @@ export default function CreateTask() {
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Task info */}
|
||||
<Card>
|
||||
<Card className="lg:w-2xl">
|
||||
<CardHeader><CardTitle className="text-base">Task Details</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
@@ -124,5 +125,6 @@ export default function CreateTask() {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -75,17 +75,18 @@ export default function EditTask() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-8 px-4 space-y-6">
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
||||
<div className="mx-auto space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
{/** /admin/taskList/${taskListId}/${taskId}/tasks */}
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
{/** /admin/taskList/${taskListId}/tasks */}
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(`/admin/taskList/${taskListId}/tasks/${taskId}/view`)}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-semibold">Edit Task</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<Card>
|
||||
<Card className="lg:w-2xl">
|
||||
<CardHeader><CardTitle className="text-base">Task Details</CardTitle></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
@@ -152,8 +153,8 @@ export default function EditTask() {
|
||||
</Card>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
{/** /admin/taskList/${taskListId}/${taskId}/tasks */}
|
||||
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
|
||||
{/** /admin/taskList/${taskListId}/tasks */}
|
||||
<Button type="button" variant="outline" onClick={() => navigate(`/admin/taskList/${taskListId}/tasks/${taskId}/view`)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
@@ -162,5 +163,6 @@ export default function EditTask() {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export default function Tasks() {
|
||||
const {
|
||||
taskList, tasks, attributes, pagination, loading,
|
||||
fetchTaskList, fetchTasks, fetchArchivedTasks,
|
||||
archiveTask, restoreTask,
|
||||
archiveTask, restoreTask, fetchTaskFieldValues,
|
||||
bulkArchiveTasks, bulkRestoreTasks,
|
||||
} = useAdminTask();
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function Tasks() {
|
||||
const breadcrumbs = [
|
||||
{ label: 'Home', icon: <House className="size-4" />, to: '/admin' },
|
||||
{ label: 'Task List', to: '/admin/taskList' },
|
||||
{ label: 'View Tasks' },
|
||||
{ label: 'Tasks' },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -214,6 +214,7 @@ export default function Tasks() {
|
||||
pagination={pagination}
|
||||
loading={loading}
|
||||
onFetch={handleFetch}
|
||||
onFetchFilterData={fetchTaskFieldValues}
|
||||
toolbarActions={toolbarActions}
|
||||
selectionActions={selectionActions}
|
||||
columnPinning={columnPinning}
|
||||
|
||||
@@ -150,7 +150,8 @@ export default function ViewTask() {
|
||||
const requirements = task.requirements ?? [];
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto p-4">
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
||||
<div className="mx-auto p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
@@ -177,7 +178,7 @@ export default function ViewTask() {
|
||||
</div>
|
||||
|
||||
{/* Main content — plain div avoids Card overflow:hidden clipping */}
|
||||
<div className="rounded-lg border border-border bg-card text-card-foreground shadow-sm">
|
||||
<div className="lg:w-2xl rounded-lg border border-border bg-card text-card-foreground shadow-sm">
|
||||
<div className="p-4 space-y-2">
|
||||
|
||||
{/* Description */}
|
||||
@@ -238,5 +239,6 @@ export default function ViewTask() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -158,6 +158,8 @@ export const AdminRoutes = {
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// Task
|
||||
{
|
||||
path: 'taskList',
|
||||
|
||||
Reference in New Issue
Block a user