Files
starr-philproperties/src/modules/admin/pages/task_list/ViewTaskList.jsx
T
2026-08-03 17:19:12 +08:00

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>
);
}