Files
eene_dashboard/frontend/src/components/dashboard/TaskCard.tsx
EENE Dashboard cf72281c6d feat: quarter board theme, hub column, and team panel UX
Apply preview-style 4-dept layout with center hub, PM/assignee team status linking, task type label updates, and remove task keywords.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 22:09:46 +09:00

172 lines
5.1 KiB
TypeScript

import { useRef } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { DraggableAttributes } from '@dnd-kit/core';
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
import type { Task } from '../../types';
import { DonutGauge } from './DonutGauge';
function fmtDate(iso: string | null | undefined): string {
if (!iso) return '';
const d = new Date(iso);
return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
}
function fmtDateRange(task: Task): string {
if (!task.showDate || (!task.startDate && !task.dueDate)) return '';
const start = task.startDate ? fmtDate(task.startDate) : '?';
const end = task.dueDate ? fmtDate(task.dueDate) : '?';
return `${start} ~ ${end}`;
}
function firstDescriptionLine(text: string | null | undefined): string {
if (!text) return '';
const line = text.split('\n').map((l) => l.replace(/^[•·\-]\s*/, '').trim()).find(Boolean);
return line ?? '';
}
type SectionOption = { value: string; label: string };
export function SortableTaskCard({
task,
variant = 'project',
onSelect,
}: {
task: Task;
variant?: 'project' | 'routine';
sectionOptions?: SectionOption[];
accent?: string;
onSelect?: (task: Task) => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id });
const pointerStart = useRef<{ x: number; y: number } | null>(null);
const handlePointerDown = (e: React.PointerEvent) => {
if (e.button !== 0) return;
pointerStart.current = { x: e.clientX, y: e.clientY };
listeners?.onPointerDown?.(e);
};
const handlePointerUp = (e: React.PointerEvent) => {
if (e.button !== 0 || !pointerStart.current) return;
const dx = e.clientX - pointerStart.current.x;
const dy = e.clientY - pointerStart.current.y;
pointerStart.current = null;
if (!isDragging && Math.hypot(dx, dy) < 8) {
onSelect?.(task);
}
};
return (
<TaskCard
task={task}
variant={variant}
dragRef={setNodeRef}
dragStyle={{
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.35 : 1,
}}
dragAttributes={attributes}
dragListeners={listeners}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
/>
);
}
export function TaskCard({
task,
variant = 'project',
dragRef,
dragStyle,
dragAttributes,
dragListeners,
onPointerDown,
onPointerUp,
}: {
task: Task;
variant?: 'project' | 'routine';
dragRef?: (node: HTMLElement | null) => void;
dragStyle?: React.CSSProperties;
dragAttributes?: DraggableAttributes;
dragListeners?: SyntheticListenerMap;
onPointerDown?: (e: React.PointerEvent) => void;
onPointerUp?: (e: React.PointerEvent) => void;
}) {
const dragHandlers = {
onPointerDown: (e: React.PointerEvent) => {
onPointerDown?.(e);
},
onPointerUp: (e: React.PointerEvent) => {
onPointerUp?.(e);
},
onKeyDown: dragListeners?.onKeyDown as React.KeyboardEventHandler<HTMLDivElement> | undefined,
};
if (variant === 'routine') {
const descLine = firstDescriptionLine(task.description);
return (
<div
ref={dragRef}
style={dragStyle}
{...dragAttributes}
data-task-card="true"
data-task-id={task.id}
className="board-routine-item"
{...dragHandlers}
>
<span className="board-project-title">{task.title}</span>
{descLine && <p className="board-project-desc"> {descLine}</p>}
</div>
);
}
const dateRange = fmtDateRange(task);
const descLine = task.showDescription ? firstDescriptionLine(task.description) : '';
const showProgress = task.showProgress !== false;
return (
<article
ref={dragRef}
style={dragStyle}
{...dragAttributes}
data-task-card="true"
data-task-id={task.id}
className="project-sub-card"
{...dragHandlers}
>
<div className="project-sub-body">
<div className="project-fields">
<div className="project-sub-title">{task.title}</div>
{dateRange && (
<div className="project-field">
<span className="project-field-label"> </span>
<span className="project-field-value">{dateRange}</span>
</div>
)}
{descLine && (
<div className="project-field">
<span className="project-field-label"> </span>
<span className="project-field-value">{descLine}</span>
</div>
)}
{task.showIssue && task.issueNote && (
<div className="project-field">
<span className="project-field-label"></span>
<span className="project-field-value" style={{ color: '#c0392b' }}>
{task.issueNote}
</span>
</div>
)}
</div>
{showProgress && (
<div className="progress-col">
<DonutGauge task={task} />
</div>
)}
</div>
</article>
);
}