182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import { cn } from "@xtablo/shared";
|
|
import type { KanbanTask, TaskStatus } from "@xtablo/shared-types";
|
|
import { TaskModal } from "@xtablo/tablo-views";
|
|
import { CheckCircle2, Plus } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useTablosList } from "../hooks/tablos";
|
|
import { useAllTasks, useUpdateTask } from "../hooks/tasks";
|
|
import { useUser } from "../providers/UserStoreProvider";
|
|
|
|
type TaskWithTablo = KanbanTask & {
|
|
tablos: { id: string; name: string; color: string | null } | null;
|
|
};
|
|
|
|
const STATUS_BADGE: Record<TaskStatus, { className: string; labelKey: string }> = {
|
|
todo: {
|
|
className: "bg-blue-50 text-blue-600 dark:bg-blue-950/30 dark:text-blue-400",
|
|
labelKey: "dashboard.taskList.status.todo",
|
|
},
|
|
in_progress: {
|
|
className: "bg-yellow-50 text-yellow-600 dark:bg-yellow-950/30 dark:text-yellow-400",
|
|
labelKey: "dashboard.taskList.status.inProgress",
|
|
},
|
|
in_review: {
|
|
className: "bg-purple-50 text-purple-600 dark:bg-purple-950/30 dark:text-purple-400",
|
|
labelKey: "dashboard.taskList.status.inReview",
|
|
},
|
|
done: {
|
|
className: "bg-green-50 text-green-600 dark:bg-green-950/30 dark:text-green-400",
|
|
labelKey: "dashboard.taskList.status.done",
|
|
},
|
|
};
|
|
|
|
function TaskRow({
|
|
task,
|
|
onToggleDone,
|
|
}: {
|
|
task: TaskWithTablo;
|
|
onToggleDone: (task: TaskWithTablo) => void;
|
|
}) {
|
|
const { t } = useTranslation("pages");
|
|
const navigate = useNavigate();
|
|
const status = task.status ?? "todo";
|
|
const isDone = status === "done";
|
|
const badge = STATUS_BADGE[status];
|
|
|
|
const dateStr = task.updated_at ?? task.created_at;
|
|
const formattedDate = dateStr
|
|
? new Intl.DateTimeFormat(undefined, {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
}).format(new Date(dateStr))
|
|
: "";
|
|
|
|
return (
|
|
<div
|
|
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors border-b border-gray-200 dark:border-gray-700 cursor-pointer"
|
|
onClick={() => {
|
|
if (task.tablos) {
|
|
navigate(`/tablos/${task.tablos.id}?section=tasks`);
|
|
}
|
|
}}
|
|
>
|
|
{/* Checkbox */}
|
|
<button
|
|
className={cn(
|
|
"w-8 h-8 min-h-[44px] min-w-[44px] rounded-full border-2 flex items-center justify-center shrink-0",
|
|
isDone
|
|
? "bg-purple-600 border-purple-600"
|
|
: "border-gray-300 hover:border-purple-400 dark:border-gray-600 dark:hover:border-purple-500"
|
|
)}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleDone(task);
|
|
}}
|
|
>
|
|
{isDone && <CheckCircle2 className="w-4 h-4 text-white" />}
|
|
</button>
|
|
|
|
{/* Title + Tablo (stacked on mobile) */}
|
|
<div className="flex-1 min-w-0">
|
|
<p
|
|
className={cn(
|
|
"text-sm font-medium truncate",
|
|
isDone
|
|
? "line-through text-gray-400 dark:text-gray-500"
|
|
: "text-gray-900 dark:text-gray-100"
|
|
)}
|
|
>
|
|
{task.title}
|
|
</p>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
{task.tablos && (
|
|
<>
|
|
<div
|
|
className={cn(
|
|
"w-4 h-4 rounded flex items-center justify-center text-xs shrink-0",
|
|
task.tablos.color || "bg-gray-400"
|
|
)}
|
|
>
|
|
<span className="text-white font-bold text-[8px]">
|
|
{task.tablos.name.charAt(0).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<span className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
|
{task.tablos.name}
|
|
</span>
|
|
</>
|
|
)}
|
|
{formattedDate && (
|
|
<span className="text-xs text-gray-400 dark:text-gray-500 hidden sm:inline whitespace-nowrap">
|
|
{formattedDate}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status badge */}
|
|
<span
|
|
className={cn(
|
|
"px-2 sm:px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap shrink-0",
|
|
badge.className
|
|
)}
|
|
>
|
|
{t(badge.labelKey)}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DashboardTaskList() {
|
|
const { t } = useTranslation("pages");
|
|
const user = useUser();
|
|
const { data: allTasks } = useAllTasks();
|
|
const { data: tablos } = useTablosList();
|
|
const updateTask = useUpdateTask();
|
|
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
|
|
|
// Filter to tasks assigned to the current user, limited to recent ones
|
|
const myTasks = allTasks?.filter((task) => task.assignee_id === user.id).slice(0, 7) ?? [];
|
|
|
|
const handleToggleDone = (task: TaskWithTablo) => {
|
|
const newStatus: TaskStatus = task.status === "done" ? "todo" : "done";
|
|
updateTask.mutate({ id: task.id, status: newStatus });
|
|
};
|
|
|
|
if (myTasks.length === 0) return null;
|
|
|
|
return (
|
|
<>
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl border border-gray-100 dark:border-gray-700">
|
|
<div className="flex items-center justify-between px-4 py-4 sm:py-5 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
{t("dashboard.taskList.title")}
|
|
</h2>
|
|
<button
|
|
className="flex items-center gap-2 px-4 py-2.5 min-h-[44px] bg-white dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
|
|
onClick={() => setIsTaskModalOpen(true)}
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>{t("dashboard.taskList.addTask")}</span>
|
|
</button>
|
|
</div>
|
|
<div>
|
|
{myTasks.map((task) => (
|
|
<TaskRow key={task.id} task={task} onToggleDone={handleToggleDone} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<TaskModal
|
|
isOpen={isTaskModalOpen}
|
|
onClose={() => setIsTaskModalOpen(false)}
|
|
tablos={tablos}
|
|
allowTabloSelection
|
|
initialStatus="todo"
|
|
/>
|
|
</>
|
|
);
|
|
}
|