177 lines
7.3 KiB
TypeScript
177 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Types */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
interface ActivityItem {
|
|
id: string;
|
|
time: string;
|
|
title: string;
|
|
description: string;
|
|
iconGradient: string;
|
|
icon: React.ReactNode;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Icons */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
const InboundIcon = (
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-white" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
</svg>
|
|
);
|
|
|
|
const OutboundIcon = (
|
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-white" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0H6.75m11.25 0h2.625c.621 0 1.125-.504 1.125-1.125v-4.875c0-.621-.504-1.125-1.125-1.125H17.25m-13.5-.375V6.375c0-.621.504-1.125 1.125-1.125h7.5c.621 0 1.125.504 1.125 1.125v7.125" />
|
|
</svg>
|
|
);
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helper */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function formatTime(dateStr: string): string {
|
|
if (!dateStr) return "";
|
|
try {
|
|
const d = new Date(dateStr);
|
|
if (isNaN(d.getTime())) return "";
|
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Component */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export function RecentActivity() {
|
|
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchActivity = async () => {
|
|
try {
|
|
const [inboundRes, outboundRes] = await Promise.all([
|
|
apiClient.get("/receiving/list", {
|
|
params: {},
|
|
}),
|
|
apiClient.get("/outbound/list", {
|
|
params: {},
|
|
}),
|
|
]);
|
|
|
|
const inboundData: Record<string, unknown>[] = inboundRes.data?.data ?? [];
|
|
const outboundData: Record<string, unknown>[] = outboundRes.data?.data ?? [];
|
|
|
|
// Map inbound items
|
|
const inboundItems: ActivityItem[] = inboundData.slice(0, 10).map((r, idx) => ({
|
|
id: `in-${r.id || idx}`,
|
|
time: formatTime(String(r.created_date ?? "")),
|
|
title: `입고 ${String(r.inbound_number ?? "")}`,
|
|
description: [
|
|
r.supplier_name ? String(r.supplier_name) : null,
|
|
r.item_name ? String(r.item_name) : null,
|
|
r.inbound_qty ? `${String(r.inbound_qty)}${r.unit ? String(r.unit) : "EA"}` : null,
|
|
].filter(Boolean).join(" | ") || "입고 처리",
|
|
iconGradient: "linear-gradient(135deg,#3b82f6,#1d4ed8)",
|
|
icon: InboundIcon,
|
|
}));
|
|
|
|
// Map outbound items
|
|
const outboundItems: ActivityItem[] = outboundData.slice(0, 10).map((r, idx) => ({
|
|
id: `out-${r.id || idx}`,
|
|
time: formatTime(String(r.created_date ?? "")),
|
|
title: `출고 ${String(r.outbound_number ?? "")}`,
|
|
description: [
|
|
r.customer_name ? String(r.customer_name) : null,
|
|
r.item_name ? String(r.item_name) : null,
|
|
r.outbound_qty ? `${String(r.outbound_qty)}${r.unit ? String(r.unit) : "EA"}` : null,
|
|
].filter(Boolean).join(" | ") || "출고 처리",
|
|
iconGradient: "linear-gradient(135deg,#22c55e,#15803d)",
|
|
icon: OutboundIcon,
|
|
}));
|
|
|
|
// Merge and sort by time descending, take top 5
|
|
const merged = [...inboundItems, ...outboundItems].sort((a, b) => {
|
|
// Sort by time string descending (HH:MM)
|
|
if (b.time > a.time) return 1;
|
|
if (b.time < a.time) return -1;
|
|
return 0;
|
|
});
|
|
|
|
setActivities(merged.slice(0, 5));
|
|
} catch {
|
|
// On failure, show empty state
|
|
setActivities([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchActivity();
|
|
}, []);
|
|
|
|
return (
|
|
<section>
|
|
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 sm:p-6">
|
|
<div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-100">
|
|
<h3 className="text-base sm:text-lg font-bold text-gray-900">최근 활동</h3>
|
|
<span className="text-xs text-gray-400 cursor-pointer hover:text-gray-600 transition-colors">
|
|
전체 보기
|
|
</span>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8 text-sm text-gray-400">
|
|
<svg className="animate-spin w-5 h-5 mr-2 text-blue-500" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
불러오는 중...
|
|
</div>
|
|
) : activities.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-8 text-gray-400">
|
|
<svg className="w-10 h-10 mb-2 opacity-30" fill="none" stroke="currentColor" strokeWidth={1} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<p className="text-sm">최근 활동이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-2 sm:gap-3">
|
|
{activities.map((item, idx) => (
|
|
<div
|
|
key={`${item.id}-${idx}`}
|
|
className="flex items-center gap-3 sm:gap-4 p-3 rounded-xl transition-all duration-150 hover:bg-gray-50 hover:translate-x-1"
|
|
>
|
|
<span
|
|
className="text-xs sm:text-sm font-semibold text-gray-400 min-w-[44px] sm:min-w-[52px] text-right"
|
|
style={{ fontVariantNumeric: "tabular-nums" }}
|
|
>
|
|
{item.time}
|
|
</span>
|
|
<div
|
|
className="w-9 h-9 sm:w-10 sm:h-10 rounded-full flex items-center justify-center shrink-0"
|
|
style={{ background: item.iconGradient }}
|
|
>
|
|
{item.icon}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-semibold text-gray-900 truncate">{item.title}</div>
|
|
<div className="text-xs text-gray-400 mt-0.5 truncate">{item.description}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|