283 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|