Files
vexplor_dev/frontend/components/pop/hardcoded/RecentActivity.tsx
kjs 57b64653c0 merge: resolve conflicts accepting incoming changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:44:17 +09:00

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>
);
}