You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
// frontend/components/tasks/ListView.tsx
|
|
"use client";
|
|
import { Icon } from "@/components/Icon";
|
|
import { cx } from "@/lib/cx";
|
|
import { COLUMNS, STATUS_META, dueDay, dueLabel, isSoon, pct, stat } from "@/lib/tasks/tree";
|
|
import type { Person, Project, Task } from "@/lib/types";
|
|
import { Av, Tag } from "./bits";
|
|
|
|
function LRow({
|
|
t,
|
|
people,
|
|
projects,
|
|
onOpen,
|
|
}: {
|
|
t: Task;
|
|
people: Record<string, Person>;
|
|
projects: Record<string, Project>;
|
|
onOpen: (id: string) => void;
|
|
}) {
|
|
const s = stat(t);
|
|
const sm = STATUS_META[t.status];
|
|
const soon = isSoon(t);
|
|
return (
|
|
<div className={cx("lrow", t.status === "done" && "done-row")} onClick={() => onOpen(t.id)}>
|
|
<div className="lt">
|
|
<span className="lt-status" style={{ background: sm.c }} />
|
|
<span className="lt-title">{t.title}</span>
|
|
</div>
|
|
<div className="lcell hide-sm">
|
|
<Tag project={projects[t.project_id]} />
|
|
</div>
|
|
<div className="lcell center hide-sm">
|
|
<Av person={t.assignee_id ? people[t.assignee_id] : null} />
|
|
</div>
|
|
<div className="lcell">
|
|
<span className={cx("due", soon && "soon")}>
|
|
<Icon name="cal" /> {dueLabel(t.due)}
|
|
</span>
|
|
</div>
|
|
<div className="lcell hide-sm">
|
|
<span className={cx("prio-flag", `prio-${t.prio}`)}>
|
|
<Icon name="flag" /> {t.prio}
|
|
</span>
|
|
</div>
|
|
<div className="lcell">
|
|
{s.total > 0 ? (
|
|
<span className="lprog">
|
|
<span className="bar">
|
|
<span style={{ width: pct(t) + "%" }} />
|
|
</span>
|
|
<span className="n">
|
|
{s.done}/{s.total}
|
|
</span>
|
|
</span>
|
|
) : (
|
|
<span className="n" style={{ color: "var(--faint)" }}>
|
|
—
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ListView({
|
|
tasks,
|
|
people,
|
|
projects,
|
|
onOpen,
|
|
}: {
|
|
tasks: Task[];
|
|
people: Record<string, Person>;
|
|
projects: Record<string, Project>;
|
|
onOpen: (id: string) => void;
|
|
}) {
|
|
const groups = COLUMNS.map((col) => ({
|
|
...col,
|
|
items: tasks
|
|
.filter((t) => t.status === col.id)
|
|
.sort((a, b) => dueDay(a.due) - dueDay(b.due)),
|
|
})).filter((g) => g.items.length > 0);
|
|
return (
|
|
<div className="lgroups">
|
|
{groups.map((g) => (
|
|
<div className="lgroup" key={g.id}>
|
|
<div className="lgroup-head">
|
|
<span className="lgroup-dot" style={{ background: g.accent }} />
|
|
<span className="lgroup-label">{g.label}</span>
|
|
<span className="lgroup-count">{g.items.length}</span>
|
|
</div>
|
|
<div className="ltable">
|
|
<div className="lhead">
|
|
<div>작업</div>
|
|
<div className="hide-sm">프로젝트</div>
|
|
<div className="center hide-sm">담당</div>
|
|
<div>마감</div>
|
|
<div className="hide-sm">우선순위</div>
|
|
<div>진행</div>
|
|
</div>
|
|
{g.items.map((t) => (
|
|
<LRow key={t.id} t={t} people={people} projects={projects} onOpen={onOpen} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{groups.length === 0 && <div className="lempty">표시할 작업이 없어요</div>}
|
|
</div>
|
|
);
|
|
}
|