mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
218 lines
9.2 KiB
React
218 lines
9.2 KiB
React
import { useEffect, useState } from 'react';
|
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
|
|
import { useAdminTask } from '@/contexts/AdminTaskContext';
|
|
import { useDateFormat } from '@/hooks/useDateFormat';
|
|
import { PageMeta } from '@/contexts/MetadataContext';
|
|
|
|
import TasksTable from '../../components/task_list/TasksTable';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import {
|
|
ArrowLeft, Pencil, Users, ListTodo, Info,
|
|
NotebookPen, FileText, CalendarClock,
|
|
} from 'lucide-react';
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function InfoRow({ label, children }) {
|
|
return (
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wide">{label}</span>
|
|
<span className="text-sm font-medium">{children ?? <span className="text-muted-foreground italic">—</span>}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SectionCard({ icon: Icon, title, children }) {
|
|
return (
|
|
<div className="rounded-lg border bg-card p-5 space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
|
|
<h2 className="text-sm font-semibold">{title}</h2>
|
|
</div>
|
|
<Separator />
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function LoadingSkeleton() {
|
|
return (
|
|
<div className="space-y-5">
|
|
<Skeleton className="h-32 w-full" />
|
|
<Skeleton className="h-32 w-full" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Tab: Details ───────────────────────────────────────────────────────────
|
|
|
|
function DetailsTab({ taskList }) {
|
|
const { fmtDateTime } = useDateFormat();
|
|
|
|
if (!taskList) return <LoadingSkeleton />;
|
|
|
|
const groups = taskList.groups ?? [];
|
|
const taskCount = taskList.tasks?.length ?? 0;
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<SectionCard icon={FileText} title="Basic Information">
|
|
<div className="space-y-4">
|
|
<InfoRow label="Name">{taskList.name}</InfoRow>
|
|
{taskList.description && (
|
|
<InfoRow label="Description">
|
|
<span className="whitespace-pre-wrap text-sm font-normal text-foreground">
|
|
{taskList.description}
|
|
</span>
|
|
</InfoRow>
|
|
)}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard icon={ListTodo} title="Stats">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<InfoRow label="Tasks">
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<ListTodo className="h-3.5 w-3.5 text-muted-foreground" />
|
|
{taskCount}
|
|
</div>
|
|
</InfoRow>
|
|
<InfoRow label="Groups">
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<Users className="h-3.5 w-3.5 text-muted-foreground" />
|
|
{taskList.group_count ?? groups.length}
|
|
</div>
|
|
</InfoRow>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard icon={Users} title="Assigned Groups">
|
|
{groups.length > 0 ? (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{groups.map((g) => (
|
|
<Badge key={g.group_id} variant="secondary">
|
|
<Users className="size-3 mr-1" />
|
|
{g.name ?? g.group_id}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No groups assigned.</p>
|
|
)}
|
|
</SectionCard>
|
|
|
|
<SectionCard icon={CalendarClock} title="Audit">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<InfoRow label="Created At">
|
|
{taskList.createdAt ? fmtDateTime(taskList.createdAt) : '—'}
|
|
</InfoRow>
|
|
<InfoRow label="Updated At">
|
|
{taskList.updatedAt ? fmtDateTime(taskList.updatedAt) : '—'}
|
|
</InfoRow>
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Tabs config ────────────────────────────────────────────────────────────
|
|
|
|
const TABS = [
|
|
{ key: 'details', label: 'Details', icon: Info },
|
|
{ key: 'tasks', label: 'Tasks', icon: NotebookPen },
|
|
];
|
|
|
|
// ─── Page ───────────────────────────────────────────────────────────────────
|
|
|
|
export default function ViewTaskList() {
|
|
const navigate = useNavigate();
|
|
const { taskListId } = useParams();
|
|
const [searchParams] = useSearchParams();
|
|
const { fetchTaskList } = useAdminTask();
|
|
|
|
const [taskList, setTaskList] = useState(null);
|
|
const [activeTab, setActiveTab] = useState(
|
|
searchParams.get('tab') === 'tasks' ? 'tasks' : 'details'
|
|
);
|
|
|
|
useEffect(() => {
|
|
fetchTaskList(taskListId).then((data) => {
|
|
if (!data) return;
|
|
setTaskList(data);
|
|
});
|
|
}, [taskListId]);
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-screen bg-muted/60">
|
|
<PageMeta title={taskList ? `${taskList.name} - STARR` : undefined} description={taskList?.description} />
|
|
|
|
{/* ── Sticky header ── */}
|
|
<div
|
|
className="sticky z-20 shrink-0 border-b bg-white/70 dark:bg-zinc-900/70 backdrop-blur-md"
|
|
style={{ top: 'var(--navbar-h)' }}
|
|
>
|
|
<div className="lg:container lg:mx-auto lg:px-6 px-4">
|
|
<div className="flex items-center gap-3 py-3">
|
|
<Button type="button" variant="ghost" size="icon" onClick={() => navigate('/admin/taskList')}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<div className="flex-1 min-w-0">
|
|
<h1 className="text-xl font-semibold leading-tight flex items-center gap-2">
|
|
<ListTodo className="h-5 w-5 text-muted-foreground" />
|
|
View Task List
|
|
</h1>
|
|
{taskList && (
|
|
<p className="text-sm text-muted-foreground">{taskList.name}</p>
|
|
)}
|
|
</div>
|
|
{activeTab === 'details' && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => navigate(`/admin/taskList/${taskListId}/edit`)}
|
|
disabled={!taskList}
|
|
>
|
|
<Pencil className="h-4 w-4 mr-2" />
|
|
Edit
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Underline tabs */}
|
|
<div className="flex gap-1 pb-0 -mb-px">
|
|
{TABS.map(({ key, label, icon: Icon }) => (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={() => setActiveTab(key)}
|
|
className={[
|
|
'flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors',
|
|
activeTab === key
|
|
? 'border-primary text-primary'
|
|
: 'border-transparent text-muted-foreground hover:text-foreground',
|
|
].join(' ')}
|
|
>
|
|
<Icon className="h-3.5 w-3.5" />
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Content ── */}
|
|
<div className="lg:container lg:mx-auto lg:px-6 px-4 py-6">
|
|
<div className={activeTab === 'tasks' ? 'pb-16' : 'max-w-3xl mx-auto pb-16'}>
|
|
{activeTab === 'details' && <DetailsTab taskList={taskList} />}
|
|
{activeTab === 'tasks' && <TasksTable taskListId={taskListId} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|