ERP-node/frontend/components/report/designer/PageListPanel.tsx

273 lines
9.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { Plus, Copy, Trash2, Edit2, Check, X, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { MM_TO_PX } from "./ReportDesignerCanvas";
import type { ComponentConfig, ReportPage } from "@/types/report";
const THUMB_W = 80;
const TYPE_COLORS: Record<string, string> = {
text: "#6366f1",
label: "#6366f1",
table: "#0891b2",
image: "#16a34a",
divider: "#9ca3af",
signature: "#d97706",
stamp: "#d97706",
pageNumber: "#8b5cf6",
card: "#0ea5e9",
calculation: "#ec4899",
barcode: "#1e293b",
checkbox: "#f59e0b",
};
function PageThumbnail({ page }: { page: ReportPage }) {
const canvasW = page.width * MM_TO_PX;
const canvasH = page.height * MM_TO_PX;
const scale = THUMB_W / canvasW;
const thumbH = canvasH * scale;
return (
<div
className="relative overflow-hidden rounded border border-gray-200 bg-white shadow-sm"
style={{ width: THUMB_W, height: thumbH }}
>
{page.components.map((comp: ComponentConfig) => (
<div
key={comp.id}
className="absolute rounded-[1px]"
style={{
left: comp.x * scale,
top: comp.y * scale,
width: Math.max(comp.width * scale, 2),
height: Math.max(comp.height * scale, 2),
backgroundColor: TYPE_COLORS[comp.type] ?? "#94a3b8",
opacity: 0.6,
}}
/>
))}
</div>
);
}
export function PageListPanel() {
const {
layoutConfig,
currentPageId,
addPage,
deletePage,
duplicatePage,
reorderPages,
selectPage,
updatePageSettings,
isPageListCollapsed,
setIsPageListCollapsed,
} = useReportDesigner();
const [editingPageId, setEditingPageId] = useState<string | null>(null);
const [editingName, setEditingName] = useState("");
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(null);
const handleStartEdit = (pageId: string, currentName: string) => {
setEditingPageId(pageId);
setEditingName(currentName);
};
const handleSaveEdit = () => {
if (editingPageId && editingName.trim()) {
updatePageSettings(editingPageId, { page_name: editingName.trim() });
}
setEditingPageId(null);
setEditingName("");
};
const handleCancelEdit = () => {
setEditingPageId(null);
setEditingName("");
};
const handleDragStart = (e: React.DragEvent, index: number) => {
setDraggedIndex(index);
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", String(index));
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = "move";
if (draggedIndex === null || draggedIndex === index) {
setDropTargetIndex(null);
return;
}
setDropTargetIndex(index);
};
const handleDrop = (e: React.DragEvent, targetIndex: number) => {
e.preventDefault();
e.stopPropagation();
if (draggedIndex === null) return;
if (draggedIndex !== targetIndex) {
reorderPages(draggedIndex, targetIndex);
}
setDraggedIndex(null);
setDropTargetIndex(null);
};
const handleDragEnd = () => {
setDraggedIndex(null);
setDropTargetIndex(null);
};
if (isPageListCollapsed) {
return (
<div className="flex h-full w-10 shrink-0 flex-col items-center border-r border-gray-200 bg-white pt-2">
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => setIsPageListCollapsed(false)}
title="페이지 목록 열기"
>
<PanelLeftOpen className="h-4 w-4" />
</Button>
</div>
);
}
return (
<div className="flex h-full w-40 shrink-0 flex-col border-r border-gray-200 bg-white">
{/* 헤더 */}
<div className="flex h-11 items-center justify-between border-b border-gray-200 px-2">
<Button
size="icon"
variant="ghost"
className="h-7 w-7"
onClick={() => setIsPageListCollapsed(true)}
title="페이지 목록 접기"
>
<PanelLeftClose className="h-4 w-4" />
</Button>
<span className="text-sm font-bold text-gray-800"></span>
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => addPage()} title="페이지 추가">
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 페이지 목록 */}
<div className="flex-1 overflow-hidden">
<ScrollArea className="h-full p-2">
<div className="space-y-2" onDragOver={(e) => e.preventDefault()}>
{layoutConfig.pages
.sort((a, b) => a.page_order - b.page_order)
.map((page, index) => (
<div
key={page.page_id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
className={`group relative cursor-grab rounded-md border px-2 pt-1 pb-2 transition-all hover:shadow active:cursor-grabbing ${
page.page_id === currentPageId
? "border-indigo-400 bg-indigo-50 shadow-md ring-1 ring-indigo-200"
: "border-gray-100 bg-white hover:border-gray-200"
} ${draggedIndex === index ? "opacity-30" : ""} ${dropTargetIndex === index ? "border-dashed border-blue-400" : ""}`}
onClick={() => selectPage(page.page_id)}
>
{/* 상단 우측: 액션 버튼 모음 */}
<div className="flex items-center justify-end gap-1">
<Button
size="icon"
variant="ghost"
className="h-4 w-4 p-0 text-gray-400 hover:text-blue-600"
title="이름 변경"
onClick={(e) => {
e.stopPropagation();
handleStartEdit(page.page_id, page.page_name);
}}
>
<Edit2 className="h-2.5 w-2.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-4 w-4 p-0 text-gray-400 hover:text-blue-600"
title="복제"
onClick={(e) => {
e.stopPropagation();
duplicatePage(page.page_id);
}}
>
<Copy className="h-2.5 w-2.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-4 w-4 p-0 text-gray-400 hover:text-red-500"
title="삭제"
disabled={layoutConfig.pages.length <= 1}
onClick={(e) => {
e.stopPropagation();
deletePage(page.page_id);
}}
>
<Trash2 className="h-2.5 w-2.5" />
</Button>
</div>
{/* 썸네일 + 제목 (하단부 배치) */}
<div className="mt-4 flex flex-col items-center">
<PageThumbnail page={page} />
<div className="mt-2 w-full text-center">
{editingPageId === page.page_id ? (
<div className="flex items-center justify-center gap-1" onClick={(e) => e.stopPropagation()}>
<Input
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveEdit();
if (e.key === "Escape") handleCancelEdit();
}}
className="h-6 w-full px-1 text-center text-xs"
autoFocus
/>
<Button size="sm" variant="ghost" className="h-5 w-5 shrink-0 p-0 text-blue-600 hover:text-blue-700" onClick={handleSaveEdit}>
<Check className="h-3 w-3" />
</Button>
<Button size="sm" variant="ghost" className="h-5 w-5 shrink-0 p-0 text-red-500 hover:text-red-600" onClick={handleCancelEdit}>
<X className="h-3 w-3" />
</Button>
</div>
) : (
<div
className="cursor-text"
onDoubleClick={(e) => {
e.stopPropagation();
handleStartEdit(page.page_id, page.page_name);
}}
>
<span className="block text-[11px] font-semibold text-gray-700">
{index + 1}. {page.page_name}
</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
</div>
);
}