Workflow-App/Workflow/app/tasks/TaskDetail.tsx
sjcoetzeesj-coder 11d0178ab1 feat: commit Workflow app source; move CIPC credentials to env
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.
2026-10-18 15:21:45 +02:00

183 lines
7.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/tasks/TaskDetail.tsx
// Expanded panel for a single task. Shows log, result, error. Has
// retry, cancel, and delete buttons.
"use client";
import { useState } from "react";
import { TASK_TYPE_META, STATUS_LABEL, PRIORITY_LABEL } from "./columns";
import { type Task, retryTask, cancelTask, deleteTask } from "./api";
interface TaskDetailProps {
task: Task;
onClose: () => void;
onMutated: () => void;
onToast: (kind: "info" | "success" | "error" | "warn", title: string, body?: string) => void;
}
export default function TaskDetail({ task, onClose, onMutated, onToast }: TaskDetailProps) {
const [busy, setBusy] = useState<string | null>(null);
const meta = TASK_TYPE_META[task.task_type];
async function act(label: string, fn: () => Promise<any>) {
setBusy(label);
try {
await fn();
onMutated();
onToast("success", `${label} done`);
} catch (err: any) {
onToast("error", `${label} failed`, err.message);
} finally {
setBusy(null);
}
}
return (
<div
className="fixed inset-0 z-40 bg-black/40 flex items-start justify-center pt-12 px-4 overflow-y-auto"
onClick={onClose}
role="dialog"
aria-modal="true"
>
<div
className="bg-surface rounded-lg border border-border shadow-xl w-full max-w-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="p-5 space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 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 bg-blue-50 text-blue-700 border-blue-200">
{STATUS_LABEL[task.status]}
</span>
<span className="text-[10px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded border bg-amber-50 text-amber-800 border-amber-200">
{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>
<h2 className="text-xl font-bold text-fg leading-tight">{task.title}</h2>
{task.description ? (
<p className="text-sm text-muted mt-2 whitespace-pre-wrap">{task.description}</p>
) : null}
</div>
<button
type="button"
onClick={onClose}
className="text-muted hover:text-fg text-sm"
aria-label="Close"
>
✕
</button>
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<Meta label="Assignee" value={task.assignee ?? "—"} />
<Meta label="Claimed by" value={task.claimed_by ?? "—"} />
<Meta label="Created" value={formatTime(task.created)} />
<Meta label="Claimed at" value={formatTime(task.claimed_at)} />
<Meta label="Started" value={formatTime(task.started_at)} />
<Meta label="Completed" value={formatTime(task.completed_at)} />
<Meta label="Duration" value={task.duration_ms != null ? `${(task.duration_ms / 1000).toFixed(2)}s` : "—"} />
<Meta label="Created by" value={task.created_by ?? "—"} />
</div>
{task.payload && Object.keys(task.payload as object).length > 0 ? (
<Section title="Payload">
<pre className="text-xs font-mono bg-bg p-3 rounded border border-border overflow-x-auto">
{JSON.stringify(task.payload, null, 2)}
</pre>
</Section>
) : null}
{task.error ? (
<Section title="Error">
<p className="text-xs text-red-700 bg-red-50 border border-red-300 p-3 rounded">
{task.error}
</p>
</Section>
) : null}
{task.log && task.log.length > 0 ? (
<Section title={`Log (${task.log.length} entries)`}>
<pre className="text-[11px] font-mono bg-bg p-3 rounded border border-border overflow-x-auto whitespace-pre-wrap max-h-60 overflow-y-auto">
{task.log.join("\n")}
</pre>
</Section>
) : null}
{task.result ? (
<Section title="Result">
<pre className="text-xs font-mono bg-bg p-3 rounded border border-border overflow-x-auto max-h-60 overflow-y-auto">
{JSON.stringify(task.result, null, 2)}
</pre>
</Section>
) : null}
<div className="flex flex-wrap gap-2 pt-2 border-t border-border">
{(task.status === "failed" || task.status === "done") && (
<button
onClick={() => act("Retry", () => retryTask(task.id))}
disabled={busy !== null}
className="px-3 py-1.5 rounded-md bg-blue-600 text-white text-xs font-semibold hover:bg-blue-700 disabled:opacity-50"
>
{busy === "Retry" ? "Retrying…" : "↻ Retry"}
</button>
)}
{task.status === "in_progress" && (
<button
onClick={() => act("Cancel", () => cancelTask(task.id))}
disabled={busy !== null}
className="px-3 py-1.5 rounded-md bg-amber-500 text-white text-xs font-semibold hover:bg-amber-600 disabled:opacity-50"
>
{busy === "Cancel" ? "Cancelling…" : "✕ Cancel"}
</button>
)}
<button
onClick={() => {
if (confirm(`Delete "${task.title}"? This cannot be undone.`)) {
act("Delete", () => deleteTask(task.id).then(onClose));
}
}}
disabled={busy !== null}
className="px-3 py-1.5 rounded-md border border-red-300 text-red-700 text-xs font-semibold hover:bg-red-50 disabled:opacity-50"
>
{busy === "Delete" ? "Deleting…" : "🗑 Delete"}
</button>
</div>
</div>
</div>
</div>
);
}
function Meta({ label, value }: { label: string; value: string }) {
return (
<div>
<p className="text-[10px] font-semibold text-muted uppercase tracking-wider mb-0.5">{label}</p>
<p className="text-fg">{value}</p>
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div>
<h3 className="text-xs font-semibold text-muted uppercase tracking-wider mb-1.5">{title}</h3>
{children}
</div>
);
}
function formatTime(iso: string | null): string {
if (!iso) return "—";
const t = new Date(iso.includes("T") ? iso : iso.replace(" ", "T") + "Z");
if (isNaN(t.getTime())) return "—";
return t.toLocaleString();
}