231 lines
7.0 KiB
TypeScript
231 lines
7.0 KiB
TypeScript
import { useEffect, useMemo, useState, type ReactElement } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { Layout } from "@/components/layout/Layout";
|
|
import {
|
|
DataTable,
|
|
Pagination,
|
|
type Column,
|
|
} from "@/components/shared";
|
|
import { workflowService } from "@/services/workflow-service";
|
|
import type { WorkflowTask, WorkflowTaskCounts } from "@/types/workflow";
|
|
import { cn } from "@/lib/utils";
|
|
import { Inbox, Clock, Calendar, CheckCircle2 } from "lucide-react";
|
|
|
|
const formatDate = (value?: string | null): string => {
|
|
if (!value) return "-";
|
|
return new Date(value).toLocaleDateString("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit"
|
|
});
|
|
};
|
|
|
|
const StatCard = ({ icon: Icon, label, value, color }: { icon: any, label: string, value: number, color: string }) => (
|
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl p-4 flex items-center gap-4 shadow-sm">
|
|
<div className={cn("w-12 h-12 rounded-lg flex items-center justify-center", color)}>
|
|
<Icon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-bold text-gray-900">{value}</div>
|
|
<div className="text-sm font-medium text-gray-500">{label}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const Tasks = (): ReactElement => {
|
|
const navigate = useNavigate();
|
|
const [tasks, setTasks] = useState<WorkflowTask[]>([]);
|
|
const [counts, setCounts] = useState<WorkflowTaskCounts | null>(null);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [limit, setLimit] = useState(10);
|
|
const [total, setTotal] = useState(0);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const offset = (currentPage - 1) * limit;
|
|
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const [tasksRes, countsRes] = await Promise.all([
|
|
workflowService.listTasks({ limit, offset }),
|
|
workflowService.getTaskCounts()
|
|
]);
|
|
|
|
if (tasksRes.success) {
|
|
setTasks(tasksRes.data);
|
|
setTotal(tasksRes.pagination.total);
|
|
}
|
|
|
|
if (countsRes.success) {
|
|
setCounts(countsRes.data);
|
|
}
|
|
} catch (err: any) {
|
|
setError(err?.response?.data?.error?.message || "Failed to load tasks");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
loadData();
|
|
}, [limit, offset]);
|
|
|
|
const columns: Column<WorkflowTask>[] = useMemo(
|
|
() => [
|
|
{
|
|
key: "entity_name",
|
|
label: "Entity",
|
|
render: (task) => (
|
|
<div className="flex flex-col">
|
|
<span className="font-medium text-gray-900">{task.entity.name}</span>
|
|
<span className="text-[11px] text-gray-500 uppercase tracking-tight">{task.entity.type}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "workflow_name",
|
|
label: "Workflow",
|
|
render: (task) => (
|
|
<span className="text-gray-700">{task.workflow.name}</span>
|
|
),
|
|
},
|
|
{
|
|
key: "step_name",
|
|
label: "Step",
|
|
render: (task) => (
|
|
<div className="flex items-center gap-2">
|
|
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-[11px] font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
|
{task.step.name}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "assigned_role",
|
|
label: "Assigned To",
|
|
render: (task) => (
|
|
<span className="text-gray-600">{task.assignment.assigned_role}</span>
|
|
),
|
|
},
|
|
{
|
|
key: "status",
|
|
label: "Status",
|
|
render: (task) => (
|
|
<span className={cn(
|
|
"inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset",
|
|
task.is_overdue
|
|
? "bg-red-50 text-red-700 ring-red-600/10"
|
|
: "bg-green-50 text-green-700 ring-green-600/10"
|
|
)}>
|
|
{task.is_overdue ? "Overdue" : task.status}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: "due_at",
|
|
label: "Due Date",
|
|
render: (task) => (
|
|
<span className={cn("text-sm", task.is_overdue ? "text-red-600 font-medium" : "text-gray-600")}>
|
|
{formatDate(task.due_at)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: "actions",
|
|
label: "",
|
|
render: (task) => (
|
|
<button
|
|
onClick={() => {
|
|
if (task.entity.type.toLowerCase() === 'document') {
|
|
navigate(`/tenant/documents/${task.entity.id}`);
|
|
}
|
|
}}
|
|
className="text-[#084cc8] hover:text-[#063ba1] font-bold text-sm"
|
|
>
|
|
View
|
|
</button>
|
|
),
|
|
},
|
|
],
|
|
[navigate],
|
|
);
|
|
|
|
return (
|
|
<Layout
|
|
currentPage="My Tasks"
|
|
pageHeader={{
|
|
title: "Workflows & Tasks",
|
|
description: "Manage your pending workflow tasks and approvals.",
|
|
}}
|
|
>
|
|
<div className="flex flex-col gap-6 pb-8">
|
|
{/* Count Stats Area */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<StatCard
|
|
icon={Inbox}
|
|
label="Pending Tasks"
|
|
value={counts?.pending || 0}
|
|
color="bg-blue-600"
|
|
/>
|
|
<StatCard
|
|
icon={Clock}
|
|
label="Overdue"
|
|
value={counts?.overdue || 0}
|
|
color="bg-red-500"
|
|
/>
|
|
<StatCard
|
|
icon={Calendar}
|
|
label="Due Soon"
|
|
value={counts?.due_soon || 0}
|
|
color="bg-yellow-500"
|
|
/>
|
|
<StatCard
|
|
icon={CheckCircle2}
|
|
label="Completed (Week)"
|
|
value={counts?.completed_this_week || 0}
|
|
color="bg-green-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Task Table Area */}
|
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
<h3 className="text-lg font-bold text-gray-900">Pending Tasks</h3>
|
|
</div>
|
|
|
|
<DataTable
|
|
data={tasks}
|
|
columns={columns}
|
|
keyExtractor={(task) => task.id}
|
|
emptyMessage="No tasks currently assigned to you"
|
|
isLoading={isLoading}
|
|
error={error}
|
|
/>
|
|
|
|
{total > 0 && (
|
|
<div className="px-6 py-4 border-t border-gray-100">
|
|
<Pagination
|
|
currentPage={currentPage}
|
|
totalPages={totalPages}
|
|
totalItems={total}
|
|
limit={limit}
|
|
onPageChange={setCurrentPage}
|
|
onLimitChange={(value) => {
|
|
setLimit(value);
|
|
setCurrentPage(1);
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default Tasks;
|