ERP-node/frontend/components/report/designer/modals/FooterAggregateModal.tsx

137 lines
5.2 KiB
TypeScript

"use client";
/**
* FooterAggregateModal — 테이블 푸터 집계 설정 모달
*
* design-system.md Shell 패턴 적용.
* 특정 열을 클릭하면 열리며, 해당 열의 집계 유형(합계/평균/개수/수식)을 설정.
*/
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useUnsavedChangesGuard, UnsavedChangesDialog } from "@/components/common/UnsavedChangesGuard";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calculator, X } from "lucide-react";
import type { ComponentConfig } from "@/types/report";
type TableColumn = NonNullable<ComponentConfig["tableColumns"]>[number];
interface FooterAggregateModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
column: TableColumn | null;
columnIndex: number;
onSave: (idx: number, updates: Partial<TableColumn>) => void;
}
export function FooterAggregateModal({ open, onOpenChange, column, columnIndex, onSave }: FooterAggregateModalProps) {
const [summaryType, setSummaryType] = useState<"SUM" | "AVG" | "COUNT" | "NONE">("NONE");
const [formula, setFormula] = useState("");
const initialSnapshotRef = useRef<string>("");
const hasChanges = useCallback(() => {
const current = JSON.stringify({ summaryType, formula });
return current !== initialSnapshotRef.current;
}, [summaryType, formula]);
const guard = useUnsavedChangesGuard({
hasChanges,
onClose: () => onOpenChange(false),
});
useEffect(() => {
if (column) {
const initType = column.summaryType || "NONE";
const initFormula = column.formula || "";
setSummaryType(initType);
setFormula(initFormula);
initialSnapshotRef.current = JSON.stringify({ summaryType: initType, formula: initFormula });
}
}, [column]);
const handleSave = () => {
onSave(columnIndex, {
summaryType,
formula: summaryType === "NONE" ? undefined : formula || undefined,
});
onOpenChange(false);
};
if (!column) return null;
return (
<>
<Dialog open={open} onOpenChange={guard.handleOpenChange}>
<DialogContent className="flex h-auto max-w-lg flex-col overflow-hidden p-0 [&>button]:hidden">
<DialogTitle className="sr-only"> </DialogTitle>
<DialogDescription className="sr-only">
{column.field || column.header}
</DialogDescription>
{/* Header */}
<div className="border-border flex items-center justify-between border-b px-6 py-4">
<div className="flex items-center gap-2">
<Calculator className="h-4 w-4 text-blue-600" />
<h2 className="text-foreground text-base font-semibold"> </h2>
</div>
<Button variant="ghost" size="icon" onClick={guard.tryClose} className="h-8 w-8">
<X className="h-4 w-4" />
</Button>
</div>
{/* Content */}
<div className="space-y-3 px-6 py-4">
<p className="text-muted-foreground text-xs">
<span className="font-mono font-medium text-blue-700">{column.field || column.header}</span>
.
</p>
<div className="space-y-2">
<Label className="text-foreground text-xs font-medium"> *</Label>
<Select value={summaryType} onValueChange={(v) => setSummaryType(v as typeof summaryType)}>
<SelectTrigger className="h-9 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="NONE"></SelectItem>
<SelectItem value="SUM"></SelectItem>
<SelectItem value="AVG"></SelectItem>
<SelectItem value="COUNT"></SelectItem>
</SelectContent>
</Select>
</div>
{summaryType !== "NONE" && (
<div className="space-y-2">
<Label className="text-foreground text-xs font-medium"> ()</Label>
<Input
value={formula}
onChange={(e) => setFormula(e.target.value)}
placeholder="예: {price} * {qty}"
className="h-9 font-mono text-sm"
/>
<p className="text-muted-foreground text-[10px]"> .</p>
</div>
)}
</div>
{/* Footer */}
<div className="border-border flex items-center justify-end gap-2 border-t px-6 py-4">
<Button variant="outline" onClick={guard.tryClose}>
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleSave}>
</Button>
</div>
</DialogContent>
</Dialog>
<UnsavedChangesDialog guard={guard} />
</>
);
}