"use client"; import React, { useState, useRef, useCallback, useEffect } from "react"; import { apiClient } from "@/lib/api/client"; /* ------------------------------------------------------------------ */ /* KPI Data Types */ /* ------------------------------------------------------------------ */ interface SummaryKpi { totalWork: number; inProgress: number; completed: number; issues: number; } interface InOutKpi { inboundTotal: number; inboundCompleted: number; inboundInProgress: number; outboundTotal: number; outboundCompleted: number; outboundInProgress: number; } interface ProdQualityKpi { productionTotal: number; productionCompleted: number; productionInProgress: number; qualityRate: number | null; // null = no data } export function KpiCarousel() { const [current, setCurrent] = useState(0); const trackRef = useRef(null); const autoTimerRef = useRef | null>(null); const startXRef = useRef(0); const moveXRef = useRef(0); const draggingRef = useRef(false); /* KPI state */ const [summary, setSummary] = useState({ totalWork: 0, inProgress: 0, completed: 0, issues: 0, }); const [inOut, setInOut] = useState({ inboundTotal: 0, inboundCompleted: 0, inboundInProgress: 0, outboundTotal: 0, outboundCompleted: 0, outboundInProgress: 0, }); const [prodQuality, setProdQuality] = useState({ productionTotal: 0, productionCompleted: 0, productionInProgress: 0, qualityRate: null, }); const total = 3; /* Fetch real data */ useEffect(() => { const fetchKpi = async () => { try { const today = new Date().toISOString().slice(0, 10); const [inboundRes, outboundRes] = await Promise.all([ apiClient.get("/receiving/list", { params: { date_from: today, date_to: today }, }), apiClient.get("/outbound/list", { params: { date_from: today, date_to: today }, }), ]); const inboundData: Record[] = inboundRes.data?.data ?? []; const outboundData: Record[] = outboundRes.data?.data ?? []; // Inbound stats const inboundTotal = inboundData.length; const inboundCompleted = inboundData.filter( (r) => r.inbound_status === "완료" || r.inbound_status === "입고완료" ).length; const inboundInProgress = inboundTotal - inboundCompleted; // Outbound stats const outboundTotal = outboundData.length; const outboundCompleted = outboundData.filter( (r) => r.outbound_status === "완료" || r.outbound_status === "출고완료" ).length; const outboundInProgress = outboundTotal - outboundCompleted; // Summary KPI: total = inbound + outbound const totalWork = inboundTotal + outboundTotal; const completed = inboundCompleted + outboundCompleted; const inProgress = inboundInProgress + outboundInProgress; setSummary({ totalWork, inProgress, completed, issues: 0, }); setInOut({ inboundTotal, inboundCompleted, inboundInProgress, outboundTotal, outboundCompleted, outboundInProgress, }); // Production / Quality - no dedicated API yet, set to 0 setProdQuality({ productionTotal: 0, productionCompleted: 0, productionInProgress: 0, qualityRate: null, }); } catch { // On failure, keep zeros (no dummy data) } }; fetchKpi(); }, []); const goTo = useCallback( (idx: number) => { const next = ((idx % total) + total) % total; setCurrent(next); }, [total] ); const startAuto = useCallback(() => { if (autoTimerRef.current) clearInterval(autoTimerRef.current); autoTimerRef.current = setInterval(() => { setCurrent((prev) => ((prev + 1) % total + total) % total); }, 5000); }, [total]); const stopAuto = useCallback(() => { if (autoTimerRef.current) { clearInterval(autoTimerRef.current); autoTimerRef.current = null; } }, []); useEffect(() => { startAuto(); return () => stopAuto(); }, [startAuto, stopAuto]); // Touch handlers const handleTouchStart = (e: React.TouchEvent) => { startXRef.current = e.touches[0].clientX; draggingRef.current = true; stopAuto(); }; const handleTouchMove = (e: React.TouchEvent) => { if (!draggingRef.current) return; moveXRef.current = e.touches[0].clientX - startXRef.current; }; const handleTouchEnd = () => { if (!draggingRef.current) return; draggingRef.current = false; if (Math.abs(moveXRef.current) > 50) { if (moveXRef.current < 0) goTo(current + 1); else goTo(current - 1); } moveXRef.current = 0; startAuto(); }; // Mouse handlers (desktop drag) const handleMouseDown = (e: React.MouseEvent) => { startXRef.current = e.clientX; draggingRef.current = true; stopAuto(); e.preventDefault(); }; const handleMouseMove = (e: React.MouseEvent) => { if (!draggingRef.current) return; moveXRef.current = e.clientX - startXRef.current; }; const handleMouseUp = () => { if (!draggingRef.current) return; draggingRef.current = false; if (Math.abs(moveXRef.current) > 50) { if (moveXRef.current < 0) goTo(current + 1); else goTo(current - 1); } moveXRef.current = 0; startAuto(); }; const handleMouseLeave = () => { if (draggingRef.current) { draggingRef.current = false; moveXRef.current = 0; startAuto(); } }; const handleDotClick = (idx: number) => { stopAuto(); goTo(idx); startAuto(); }; /* Computed slide values */ const inboundPercent = inOut.inboundTotal > 0 ? Math.round((inOut.inboundCompleted / inOut.inboundTotal) * 100) : 0; const outboundPercent = inOut.outboundTotal > 0 ? Math.round((inOut.outboundCompleted / inOut.outboundTotal) * 100) : 0; const productionPercent = prodQuality.productionTotal > 0 ? Math.round((prodQuality.productionCompleted / prodQuality.productionTotal) * 100) : 0; return (

오늘의 현황

{/* Carousel container */}
{/* Slide 1: Summary KPI */}
{/* Slide 2: Receiving + Shipping */}
} iconGradient="linear-gradient(135deg,#3b82f6,#1d4ed8)" title="입고" badge={`${inOut.inboundInProgress}건 진행`} badgeColor="text-blue-600 bg-blue-50" value={inOut.inboundCompleted} total={inOut.inboundTotal} percent={inboundPercent} barColor="bg-blue-500" /> } iconGradient="linear-gradient(135deg,#22c55e,#15803d)" title="출고" badge={`${inOut.outboundInProgress}건 진행`} badgeColor="text-green-600 bg-green-50" value={inOut.outboundCompleted} total={inOut.outboundTotal} percent={outboundPercent} barColor="bg-green-500" />
{/* Slide 3: Production + Quality */}
} iconGradient="linear-gradient(135deg,#f59e0b,#d97706)" title="생산" badge={`${prodQuality.productionInProgress}건 진행`} badgeColor="text-amber-600 bg-amber-50" value={prodQuality.productionCompleted} total={prodQuality.productionTotal > 0 ? prodQuality.productionTotal : null} percent={productionPercent} barColor="bg-amber-500" /> } iconGradient="linear-gradient(135deg,#ef4444,#b91c1c)" title="품질" badge={prodQuality.qualityRate !== null ? "정상" : "데이터 없음"} badgeColor={prodQuality.qualityRate !== null ? "text-green-600 bg-green-50" : "text-gray-500 bg-gray-50"} value={prodQuality.qualityRate !== null ? prodQuality.qualityRate : 0} total={null} percent={prodQuality.qualityRate !== null ? prodQuality.qualityRate : 0} barColor="bg-green-500" valueColor={prodQuality.qualityRate !== null ? "text-green-600" : "text-gray-400"} unit={prodQuality.qualityRate !== null ? "%" : undefined} emptyText={prodQuality.qualityRate === null ? "-" : undefined} />
{/* Dots */}
{[0, 1, 2].map((idx) => (
); } /* ---- Sub-components ---- */ function KpiCell({ value, label, color, labelColor, border = false, }: { value: string; label: string; color: string; labelColor: string; border?: boolean; }) { return (
{value} {label}
); } function KpiProgressCard({ icon, iconGradient, title, badge, badgeColor, value, total, percent, barColor, valueColor = "text-gray-900", unit, emptyText, }: { icon: React.ReactNode; iconGradient: string; title: string; badge: string; badgeColor: string; value: number; total: number | null; percent: number; barColor: string; valueColor?: string; unit?: string; emptyText?: string; }) { return (
{icon}
{title}
{badge}
{emptyText ?? value} {total !== null && ( / {total} )} {unit && {unit}}
); }