ERP-node/frontend/lib/registry/components/v2-timeline-scheduler/components/SchedulePreviewDialog.tsx

283 lines
12 KiB
TypeScript

"use client";
import React from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2, AlertTriangle, Check, X, Trash2, Play } from "lucide-react";
import { cn } from "@/lib/utils";
import { statusOptions } from "../config";
interface PreviewItem {
item_code: string;
item_name: string;
required_qty: number;
daily_capacity: number;
hourly_capacity: number;
production_days: number;
start_date: string;
end_date: string;
due_date: string;
order_count: number;
status: string;
}
interface ExistingSchedule {
id: string;
plan_no: string;
item_code: string;
item_name: string;
plan_qty: string;
start_date: string;
end_date: string;
status: string;
completed_qty?: string;
}
interface PreviewSummary {
total: number;
new_count: number;
kept_count: number;
deleted_count: number;
}
interface SchedulePreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
isLoading: boolean;
summary: PreviewSummary | null;
previews: PreviewItem[];
deletedSchedules: ExistingSchedule[];
keptSchedules: ExistingSchedule[];
onConfirm: () => void;
isApplying: boolean;
title?: string;
description?: string;
}
const summaryCards = [
{ key: "total", label: "총 계획", color: "bg-primary/10 text-primary" },
{ key: "new_count", label: "신규 입력", color: "bg-emerald-50 text-emerald-600 dark:bg-emerald-950 dark:text-emerald-400" },
{ key: "deleted_count", label: "삭제될", color: "bg-destructive/10 text-destructive" },
{ key: "kept_count", label: "유지(진행중)", color: "bg-amber-50 text-amber-600 dark:bg-amber-950 dark:text-amber-400" },
];
function formatDate(d: string | null | undefined): string {
if (!d) return "-";
const s = typeof d === "string" ? d : String(d);
return s.split("T")[0];
}
export function SchedulePreviewDialog({
open,
onOpenChange,
isLoading,
summary,
previews,
deletedSchedules,
keptSchedules,
onConfirm,
isApplying,
title,
description,
}: SchedulePreviewDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[640px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
{title || "생산계획 변경사항 확인"}
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{description || "변경사항을 확인해주세요"}
</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<span className="ml-2 text-sm text-muted-foreground"> ...</span>
</div>
) : summary ? (
<div className="max-h-[60vh] space-y-4 overflow-y-auto">
{/* 경고 배너 */}
<div className="flex items-start gap-2 rounded-md bg-amber-50 px-3 py-2 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<div className="text-xs sm:text-sm">
<p className="font-medium"> </p>
<p className="mt-0.5 text-[10px] text-amber-700 dark:text-amber-400 sm:text-xs">
.
</p>
</div>
</div>
{/* 요약 카드 */}
<div className="grid grid-cols-4 gap-2">
{summaryCards.map((card) => (
<div
key={card.key}
className={cn("rounded-lg px-3 py-3 text-center", card.color)}
>
<p className="text-lg font-bold sm:text-xl">
{(summary as any)[card.key] ?? 0}
</p>
<p className="text-[10px] sm:text-xs">{card.label}</p>
</div>
))}
</div>
{/* 신규 생성 목록 */}
{previews.length > 0 && (
<div>
<p className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-emerald-600 sm:text-sm">
<Check className="h-3.5 w-3.5" />
({previews.length})
</p>
<div className="space-y-2">
{previews.map((item, idx) => {
const statusInfo = statusOptions.find((s) => s.value === item.status);
return (
<div key={idx} className="rounded-md border border-emerald-200 bg-emerald-50/50 px-3 py-2 dark:border-emerald-800 dark:bg-emerald-950/30">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold sm:text-sm">
{item.item_code} - {item.item_name}
</p>
<span
className="rounded-md px-2 py-0.5 text-[10px] font-medium text-white sm:text-xs"
style={{ backgroundColor: statusInfo?.color || "#3b82f6" }}
>
{statusInfo?.label || item.status}
</span>
</div>
<p className="mt-1 text-xs text-primary sm:text-sm">
: <span className="font-semibold">{(item.required_qty || (item as any).plan_qty || 0).toLocaleString()}</span> EA
</p>
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-[10px] text-muted-foreground sm:text-xs">
<span>: {formatDate(item.start_date)}</span>
<span>: {formatDate(item.end_date)}</span>
</div>
{item.order_count ? (
<p className="mt-0.5 text-[10px] text-muted-foreground sm:text-xs">
{item.order_count} ( {item.required_qty.toLocaleString()} EA)
</p>
) : (item as any).parent_item_name ? (
<p className="mt-0.5 text-[10px] text-muted-foreground sm:text-xs">
: {(item as any).parent_plan_no} ({(item as any).parent_item_name}) | BOM : {(item as any).bom_qty || 1}
</p>
) : null}
</div>
);
})}
</div>
</div>
)}
{/* 삭제될 목록 */}
{deletedSchedules.length > 0 && (
<div>
<p className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-destructive sm:text-sm">
<Trash2 className="h-3.5 w-3.5" />
({deletedSchedules.length})
</p>
<div className="space-y-2">
{deletedSchedules.map((item, idx) => (
<div key={idx} className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold sm:text-sm">
{item.item_code} - {item.item_name}
</p>
<span className="rounded-md bg-destructive/20 px-2 py-0.5 text-[10px] font-medium text-destructive sm:text-xs">
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground sm:text-sm">
{item.plan_no} | : {Number(item.plan_qty || 0).toLocaleString()} EA
</p>
<div className="mt-0.5 flex flex-wrap gap-x-4 text-[10px] text-muted-foreground sm:text-xs">
<span>: {formatDate(item.start_date)}</span>
<span>: {formatDate(item.end_date)}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* 유지될 목록 (진행중) */}
{keptSchedules.length > 0 && (
<div>
<p className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-amber-600 sm:text-sm">
<Play className="h-3.5 w-3.5" />
({keptSchedules.length})
</p>
<div className="space-y-2">
{keptSchedules.map((item, idx) => {
const statusInfo = statusOptions.find((s) => s.value === item.status);
return (
<div key={idx} className="rounded-md border border-amber-200 bg-amber-50/50 px-3 py-2 dark:border-amber-800 dark:bg-amber-950/30">
<div className="flex items-center justify-between">
<p className="text-xs font-semibold sm:text-sm">
{item.item_code} - {item.item_name}
</p>
<span
className="rounded-md px-2 py-0.5 text-[10px] font-medium text-white sm:text-xs"
style={{ backgroundColor: statusInfo?.color || "#f59e0b" }}
>
{statusInfo?.label || item.status}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground sm:text-sm">
{item.plan_no} | : {Number(item.plan_qty || 0).toLocaleString()} EA
{item.completed_qty ? ` (완료: ${Number(item.completed_qty).toLocaleString()} EA)` : ""}
</p>
<div className="mt-0.5 flex flex-wrap gap-x-4 text-[10px] text-muted-foreground sm:text-xs">
<span>: {formatDate(item.start_date)}</span>
<span>: {formatDate(item.end_date)}</span>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
) : (
<div className="py-8 text-center text-sm text-muted-foreground">
</div>
)}
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isApplying}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
<X className="mr-1 h-3.5 w-3.5" />
</Button>
<Button
onClick={onConfirm}
disabled={isLoading || isApplying || !summary || previews.length === 0}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{isApplying ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
) : (
<Check className="mr-1 h-3.5 w-3.5" />
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}