Adds the full Next.js app (app/, components/, lib/, scripts/, assets/, docs/, Dockerfile, compose, configs) to version control. Security: - CIPC BizPortal customer-code logins (Login 1 + Login 3) now read CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE / CIPC_LOGIN3_PASSWORD from .env.local instead of source. - BizPortal personal login in apply-bizportal route reads BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD. - Added .env.example as the committed template. .gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright storage_state.json (live CIPC cookies), scratch dirs, client PDFs and debug screenshots.
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
// app/tasks/TaskCard.tsx
|
||
// Compact card for a single task. Used inside the Kanban columns.
|
||
|
||
"use client";
|
||
|
||
import type { Task } from "./api";
|
||
import {
|
||
TASK_TYPE_META,
|
||
PRIORITY_COLORS,
|
||
PRIORITY_LABEL,
|
||
DONE_COLLAPSE_DAYS,
|
||
} from "./columns";
|
||
|
||
interface TaskCardProps {
|
||
task: Task;
|
||
onClick?: () => void;
|
||
onDragStart?: (e: React.DragEvent) => void;
|
||
onDragEnd?: (e: React.DragEvent) => void;
|
||
isDragging?: boolean;
|
||
}
|
||
|
||
export default function TaskCard({
|
||
task,
|
||
onClick,
|
||
onDragStart,
|
||
onDragEnd,
|
||
isDragging,
|
||
}: TaskCardProps) {
|
||
const meta = TASK_TYPE_META[task.task_type];
|
||
const priorityClass = PRIORITY_COLORS[task.priority];
|
||
const isDone = task.status === "done";
|
||
const isOld = isDone && isOlderThanDays(task.completed_at, DONE_COLLAPSE_DAYS);
|
||
|
||
return (
|
||
<div
|
||
draggable
|
||
onDragStart={onDragStart}
|
||
onDragEnd={onDragEnd}
|
||
onClick={onClick}
|
||
data-od-id={`task-card-${task.id}`}
|
||
className={`group rounded-md border border-border bg-surface p-3 cursor-grab active:cursor-grabbing transition-all ${
|
||
isDragging ? "opacity-50 scale-95" : "hover:border-accent hover:shadow-sm"
|
||
} ${isOld ? "opacity-60" : ""}`}
|
||
>
|
||
<div className="flex items-start justify-between gap-2 mb-2">
|
||
<h4 className="text-sm font-semibold text-fg leading-tight line-clamp-2">{task.title}</h4>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||
<span className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded border ${meta.tone}`}>
|
||
{meta.icon} {meta.label}
|
||
</span>
|
||
<span className={`text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded border ${priorityClass}`}>
|
||
{PRIORITY_LABEL[task.priority]}
|
||
</span>
|
||
{task.retry_count > 0 ? (
|
||
<span className="text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded border bg-orange-50 text-orange-800 border-orange-300">
|
||
retry ×{task.retry_count}
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between text-[11px] text-muted">
|
||
<span>{relativeTime(task.created)}</span>
|
||
{task.duration_ms != null ? (
|
||
<span className="font-mono">{(task.duration_ms / 1000).toFixed(1)}s</span>
|
||
) : null}
|
||
</div>
|
||
|
||
{task.error ? (
|
||
<p className="mt-2 text-[11px] text-red-700 line-clamp-2" title={task.error}>
|
||
❌ {task.error}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function relativeTime(iso: string | null): string {
|
||
if (!iso) return "—";
|
||
const t = new Date(iso.includes("T") ? iso : iso.replace(" ", "T") + "Z").getTime();
|
||
if (isNaN(t)) return "—";
|
||
const diff = Date.now() - t;
|
||
const sec = Math.floor(diff / 1000);
|
||
if (sec < 60) return `${sec}s ago`;
|
||
const min = Math.floor(sec / 60);
|
||
if (min < 60) return `${min}m ago`;
|
||
const hr = Math.floor(min / 60);
|
||
if (hr < 24) return `${hr}h ago`;
|
||
const day = Math.floor(hr / 24);
|
||
return `${day}d ago`;
|
||
}
|
||
|
||
function isOlderThanDays(iso: string | null, days: number): boolean {
|
||
if (!iso) return false;
|
||
const t = new Date(iso.includes("T") ? iso : iso.replace(" ", "T") + "Z").getTime();
|
||
if (isNaN(t)) return false;
|
||
return Date.now() - t > days * 24 * 60 * 60 * 1000;
|
||
}
|