import { useState, useMemo, useCallback } from "react";
import { FileText, ChevronDown, ChevronUp, ChevronRight, RefreshCw, Search, X } from "lucide-react";
import type { TaskDocumentWithTask, TaskDetail } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { fetchTaskDetail } from "../api";
import { useDocuments } from "../hooks/useDocuments";
export interface DocumentsViewProps {
projectId?: string;
addToast: (message: string, type?: ToastType) => void;
onOpenDetail: (task: TaskDetail) => void;
}
interface DocumentCardProps {
document: TaskDocumentWithTask;
onOpenTask: (taskId: string) => void;
}
function formatTimestamp(iso?: string): string {
if (!iso) return "";
return new Date(iso).toLocaleString();
}
function getContentPreview(content: string, maxLength: number = 200): string {
if (content.length <= maxLength) return content;
return content.substring(0, maxLength) + "…";
}
function DocumentCard({ document, onOpenTask }: DocumentCardProps) {
const [expanded, setExpanded] = useState(false);
const preview = getContentPreview(document.content);
const showExpand = document.content.length > 200;
return (
{document.key}
v{document.revision}
{document.author}
·
{formatTimestamp(document.updatedAt)}
{expanded ? (
{document.content}
) : (
{preview}
)}
{showExpand && !expanded && (
…
)}
);
}
interface TaskGroupProps {
taskId: string;
taskTitle?: string;
documents: TaskDocumentWithTask[];
onOpenTask: (taskId: string) => void;
}
function TaskGroup({ taskId, taskTitle, documents, onOpenTask }: TaskGroupProps) {
const [expanded, setExpanded] = useState(false);
return (
{expanded && (
{documents.map((doc) => (
))}
)}
);
}
export function DocumentsView({ projectId, addToast, onOpenDetail }: DocumentsViewProps) {
const [searchQuery, setSearchQuery] = useState("");
const { documents, loading, error, refresh } = useDocuments({
projectId,
searchQuery: searchQuery || undefined,
});
// Group documents by task
const groupedDocuments = useMemo(() => {
const groups = new Map();
for (const doc of documents) {
const existing = groups.get(doc.taskId) || [];
groups.set(doc.taskId, [...existing, doc]);
}
// Sort groups by the most recently updated document
return Array.from(groups.entries())
.map(([taskId, docs]) => ({
taskId,
taskTitle: docs[0].taskTitle,
documents: docs.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)),
latestUpdated: docs[0].updatedAt,
}))
.sort((a, b) => b.latestUpdated.localeCompare(a.latestUpdated));
}, [documents]);
const handleSearchChange = useCallback((e: React.ChangeEvent) => {
setSearchQuery(e.target.value);
}, []);
const clearSearch = useCallback(() => {
setSearchQuery("");
}, []);
// Wrapper to open task detail by fetching full task first
const handleOpenTask = useCallback(async (taskId: string) => {
try {
const task = await fetchTaskDetail(taskId, projectId);
onOpenDetail(task);
} catch (err) {
addToast(`Failed to open task ${taskId}`, "error");
}
}, [projectId, onOpenDetail, addToast]);
if (error) {
return (
Failed to load documents: {error}
void refresh()}>
Retry
);
}
return (
{loading ? (
) : groupedDocuments.length === 0 ? (
{searchQuery ? (
No documents match "{searchQuery}".
) : (
<>
No documents yet.
Documents are created in task detail tabs.
>
)}
) : (
{groupedDocuments.map(({ taskId, taskTitle, documents: taskDocs }) => (
))}
)}
);
}