feat: sequential task IDs (HQ-1, HQ-2, etc.)
- Add serial task_number column to tasks table
- Display HQ-{number} on cards and detail panel
- API resolveTask() supports UUID, number, or HQ-N prefix
- GET /api/tasks/:id endpoint for single task lookup
- All PATCH/POST/DELETE endpoints resolve by number or UUID
This commit is contained in:
@@ -66,18 +66,9 @@ export function TaskCard({
|
||||
isActive,
|
||||
onClick,
|
||||
}: TaskCardProps) {
|
||||
const [idCopied, setIdCopied] = useState(false);
|
||||
const actions = statusActions[task.status] || [];
|
||||
const noteCount = task.progressNotes?.length || 0;
|
||||
const shortId = task.id.slice(0, 8);
|
||||
|
||||
const handleCopyId = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(task.id).then(() => {
|
||||
setIdCopied(true);
|
||||
setTimeout(() => setIdCopied(false), 1500);
|
||||
});
|
||||
};
|
||||
const displayId = task.taskNumber ? `HQ-${task.taskNumber}` : task.id.slice(0, 8);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -104,14 +95,14 @@ export function TaskCard({
|
||||
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<span
|
||||
className="text-xs px-1.5 py-0.5 rounded font-mono bg-gray-100 text-gray-500 cursor-pointer hover:bg-gray-200 transition"
|
||||
title={`Click to copy: ${task.id}`}
|
||||
className="text-xs px-1.5 py-0.5 rounded font-mono font-bold bg-amber-100 text-amber-700 cursor-pointer hover:bg-amber-200 transition"
|
||||
title={`Click to copy: ${displayId}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(task.id);
|
||||
navigator.clipboard.writeText(displayId);
|
||||
}}
|
||||
>
|
||||
{task.id.slice(0, 8)}
|
||||
{displayId}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${priorityColors[task.priority]}`}>
|
||||
{task.priority}
|
||||
@@ -127,17 +118,6 @@ export function TaskCard({
|
||||
💬 {noteCount}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCopyId}
|
||||
className="text-xs font-mono text-gray-300 hover:text-amber-600 transition cursor-pointer ml-auto"
|
||||
title={`Copy full ID: ${task.id}`}
|
||||
>
|
||||
{idCopied ? (
|
||||
<span className="text-green-500">✓ copied</span>
|
||||
) : (
|
||||
<span>{shortId}…</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
|
||||
@@ -118,30 +118,46 @@ function ElapsedTimer({ since }: { since: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CopyableId({ id }: { id: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
function CopyableId({ id, taskNumber }: { id: string; taskNumber?: number }) {
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
const displayId = taskNumber ? `HQ-${taskNumber}` : null;
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(id).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
const handleCopy = (value: string, label: string) => {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setCopied(label);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-6 py-3 border-t border-gray-100 bg-gray-50 flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400">ID:</span>
|
||||
{displayId && (
|
||||
<>
|
||||
<span className="text-xs font-bold text-amber-700 bg-amber-100 px-2 py-0.5 rounded font-mono">{displayId}</span>
|
||||
<button
|
||||
onClick={() => handleCopy(displayId, "ref")}
|
||||
className={`text-xs px-2 py-0.5 rounded-md border transition font-medium ${
|
||||
copied === "ref"
|
||||
? "bg-green-50 text-green-600 border-green-200"
|
||||
: "bg-white text-gray-500 border-gray-200 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{copied === "ref" ? "✓" : "📋"}
|
||||
</button>
|
||||
<span className="text-gray-300">|</span>
|
||||
</>
|
||||
)}
|
||||
<code className="text-xs text-gray-400 font-mono flex-1 truncate select-all">{id}</code>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
onClick={() => handleCopy(id, "uuid")}
|
||||
className={`text-xs px-2.5 py-1 rounded-md border transition font-medium ${
|
||||
copied
|
||||
copied === "uuid"
|
||||
? "bg-green-50 text-green-600 border-green-200"
|
||||
: "bg-white text-gray-500 border-gray-200 hover:bg-gray-100 hover:text-gray-700"
|
||||
}`}
|
||||
title="Copy task ID"
|
||||
title="Copy UUID"
|
||||
>
|
||||
{copied ? "✓ Copied" : "📋 Copy"}
|
||||
{copied === "uuid" ? "✓ Copied" : "📋 Copy UUID"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -189,7 +205,10 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, hasToken }: Tas
|
||||
{task.source}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-gray-900 leading-snug">{task.title}</h2>
|
||||
<h2 className="text-lg font-bold text-gray-900 leading-snug">
|
||||
{task.taskNumber && <span className="text-amber-600 mr-2">HQ-{task.taskNumber}</span>}
|
||||
{task.title}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -308,7 +327,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, hasToken }: Tas
|
||||
)}
|
||||
|
||||
{/* Task ID - click to copy */}
|
||||
<CopyableId id={task.id} />
|
||||
<CopyableId id={task.id} taskNumber={task.taskNumber} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ProgressNote {
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
taskNumber: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
source: TaskSource;
|
||||
|
||||
Reference in New Issue
Block a user