'use client'; import { useEffect, useRef, useState } from 'react'; import type { AttachmentView, TodoItem, TodoPriority } from '@/lib/types'; import type { ProjectMemberWithUser } from '@/repositories/ProjectMemberRepository'; import { AttachmentList } from '@/components/files/AttachmentList'; import { AttachmentPicker, type AttachmentPickerHandle, } from '@/components/files/AttachmentPicker'; const PRIORITIES: TodoPriority[] = ['low', 'normal', 'high']; function parseTags(tags: string | null): string[] { if (!tags) return []; return tags .split(',') .map((t) => t.trim()) .filter((t) => t.length > 0); } /** * ToDo編集/詳細ダイアログ。 * カードクリックで開き、タイトル/説明/担当/優先度/開始日/期限/タグ/添付を * 編集・閲覧できる。完了日(完了時刻)・作成/更新日時は読み取り専用で表示。 */ export function TodoDialog({ projectId, item, members, onClose, onSaved, }: { projectId: number; item: TodoItem; members: ProjectMemberWithUser[]; onClose: () => void; onSaved: () => void | Promise; }) { const [title, setTitle] = useState(item.title); const [description, setDescription] = useState(item.description ?? ''); const [assigneeId, setAssigneeId] = useState( item.assigneeId ? String(item.assigneeId) : '' ); const [priority, setPriority] = useState(item.priority); const [startDate, setStartDate] = useState(item.startDate ?? ''); const [dueDate, setDueDate] = useState(item.dueDate ?? ''); const [tags, setTags] = useState(item.tags ?? ''); const [attachments, setAttachments] = useState([]); const [current, setCurrent] = useState(item); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [pickerLoading, setPickerLoading] = useState(false); const [error, setError] = useState(null); const pickerRef = useRef(null); // 開封時に最新のアイテム+添付を取得 useEffect(() => { let alive = true; setLoading(true); fetch(`/api/projects/${projectId}/todos/items/${item.id}`) .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (!alive || !data) return; const fetched = data.item as TodoItem; setTitle(fetched.title); setDescription(fetched.description ?? ''); setAssigneeId(fetched.assigneeId ? String(fetched.assigneeId) : ''); setPriority(fetched.priority); setStartDate(fetched.startDate ?? ''); setDueDate(fetched.dueDate ?? ''); setTags(fetched.tags ?? ''); setAttachments((data.attachments as AttachmentView[]) ?? []); setCurrent(fetched); }) .catch(() => undefined) .finally(() => alive && setLoading(false)); return () => { alive = false; }; }, [projectId, item.id]); useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); async function onSave() { setSaving(true); setError(null); const fileIds = pickerRef.current?.getFileIds() ?? []; const res = await fetch( `/api/projects/${projectId}/todos/items/${item.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, description, assigneeId: assigneeId ? Number(assigneeId) : null, priority, startDate: startDate || null, dueDate: dueDate || null, tags: tags || null, fileIds, }), } ); setSaving(false); if (res.ok) { pickerRef.current?.clear(); await onSaved(); onClose(); } else { const b = (await res.json().catch(() => null)) as { error?: { message?: string }; } | null; setError(b?.error?.message ?? '保存に失敗しました'); } } const tagList = parseTags(tags); return (
e.stopPropagation()} role="dialog" aria-modal="true" aria-label={`ToDo ${item.title} の編集`} data-testid="todo-dialog" >

ToDoの編集

{loading ? (

読み込み中...

) : (
setTitle(e.target.value)} className="mt-1 w-full rounded border px-3 py-2" data-testid="todo-title" />