ERP-node/frontend/components/dashboard/widgets/CustomerIssuesWidget.tsx

262 lines
8.8 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect } from "react";
import { DashboardElement } from "@/components/admin/dashboard/types";
2025-10-24 16:08:57 +09:00
import { getApiUrl } from "@/lib/utils/apiUrl";
interface CustomerIssuesWidgetProps {
element: DashboardElement;
}
interface Issue {
id: string | number;
issue_type?: string;
issueType?: string;
customer_name?: string;
customerName?: string;
description?: string;
priority?: string;
created_at?: string;
createdAt?: string;
status?: string;
}
/**
* /
* - /
* -
*/
export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetProps) {
const [issues, setIssues] = useState<Issue[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filterPriority, setFilterPriority] = useState<string>("all");
useEffect(() => {
loadData();
2025-10-22 13:40:15 +09:00
// 자동 새로고침 (30초마다)
const interval = setInterval(loadData, 30000);
return () => clearInterval(interval);
}, [element]);
const loadData = async () => {
if (!element?.dataSource?.query) {
setError("쿼리가 설정되지 않았습니다");
setLoading(false);
return;
}
try {
setLoading(true);
const token = localStorage.getItem("authToken");
2025-10-24 16:08:57 +09:00
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: element.dataSource.query,
connectionType: element.dataSource.connectionType || "current",
connectionId: element.dataSource.connectionId,
}),
});
if (!response.ok) throw new Error("데이터 로딩 실패");
const result = await response.json();
2025-10-22 13:40:15 +09:00
if (result.success && result.data?.rows) {
setIssues(result.data.rows);
}
2025-10-22 13:40:15 +09:00
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
} finally {
setLoading(false);
}
};
const getPriorityBadge = (priority: string) => {
const priorityLower = priority?.toLowerCase() || "";
2025-10-22 13:40:15 +09:00
if (priorityLower.includes("긴급") || priorityLower.includes("high") || priorityLower.includes("urgent")) {
return "bg-destructive text-destructive-foreground";
} else if (priorityLower.includes("보통") || priorityLower.includes("medium") || priorityLower.includes("normal")) {
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100";
} else if (priorityLower.includes("낮음") || priorityLower.includes("low")) {
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100";
}
return "bg-muted text-muted-foreground";
};
const getStatusBadge = (status: string) => {
const statusLower = status?.toLowerCase() || "";
2025-10-22 13:40:15 +09:00
if (statusLower.includes("처리중") || statusLower.includes("processing") || statusLower.includes("pending")) {
return "bg-primary text-primary-foreground";
} else if (statusLower.includes("완료") || statusLower.includes("resolved") || statusLower.includes("closed")) {
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100";
}
return "bg-muted text-muted-foreground";
};
2025-10-22 13:40:15 +09:00
const filteredIssues =
filterPriority === "all"
? issues
: issues.filter((issue) => {
const priority = (issue.priority || "").toLowerCase();
return priority.includes(filterPriority);
});
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
2025-10-22 13:40:15 +09:00
<p className="text-muted-foreground mt-2 text-sm"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
2025-10-22 13:40:15 +09:00
<div className="text-destructive text-center">
<p className="text-sm"> {error}</p>
<button
onClick={loadData}
2025-10-22 13:40:15 +09:00
className="bg-destructive/10 hover:bg-destructive/20 mt-2 rounded-md px-3 py-1 text-xs"
>
</button>
</div>
</div>
);
}
if (!element?.dataSource?.query) {
return (
<div className="flex h-full items-center justify-center">
2025-10-22 13:40:15 +09:00
<div className="text-muted-foreground text-center">
<p className="text-sm"> </p>
</div>
</div>
);
}
return (
2025-10-22 13:40:15 +09:00
<div className="bg-background flex h-full flex-col overflow-hidden p-4">
{/* 헤더 */}
<div className="mb-4 flex items-center justify-between">
2025-10-22 13:40:15 +09:00
<h3 className="text-foreground text-lg font-semibold"> /</h3>
<button
onClick={loadData}
2025-10-22 13:40:15 +09:00
className="text-muted-foreground hover:bg-accent hover:text-accent-foreground rounded-full p-1"
title="새로고침"
>
🔄
</button>
</div>
{/* 필터 버튼 */}
<div className="mb-3 flex gap-2">
<button
onClick={() => setFilterPriority("all")}
className={`rounded-md px-3 py-1 text-xs transition-colors ${
filterPriority === "all"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
</button>
<button
onClick={() => setFilterPriority("긴급")}
className={`rounded-md px-3 py-1 text-xs transition-colors ${
filterPriority === "긴급"
? "bg-destructive text-destructive-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
</button>
<button
onClick={() => setFilterPriority("보통")}
className={`rounded-md px-3 py-1 text-xs transition-colors ${
filterPriority === "보통"
? "bg-yellow-100 text-yellow-800"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
</button>
<button
onClick={() => setFilterPriority("낮음")}
className={`rounded-md px-3 py-1 text-xs transition-colors ${
filterPriority === "낮음"
? "bg-green-100 text-green-800"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
</button>
</div>
{/* 총 건수 */}
2025-10-22 13:40:15 +09:00
<div className="text-muted-foreground mb-3 text-sm">
<span className="text-foreground font-semibold">{filteredIssues.length}</span>
</div>
{/* 이슈 리스트 */}
<div className="flex-1 space-y-2 overflow-auto">
{filteredIssues.length === 0 ? (
2025-10-22 13:40:15 +09:00
<div className="text-muted-foreground flex h-full items-center justify-center text-center">
<p> </p>
</div>
) : (
filteredIssues.map((issue, index) => (
<div
key={issue.id || index}
2025-10-22 13:40:15 +09:00
className="border-border bg-card rounded-lg border p-3 transition-all hover:shadow-md"
>
<div className="mb-2 flex items-start justify-between">
<div className="flex-1">
<div className="mb-1 flex items-center gap-2">
2025-10-22 13:40:15 +09:00
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${getPriorityBadge(issue.priority || "")}`}
>
{issue.priority || "보통"}
</span>
2025-10-22 13:40:15 +09:00
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${getStatusBadge(issue.status || "")}`}
>
{issue.status || "처리중"}
</span>
</div>
2025-10-22 13:40:15 +09:00
<p className="text-foreground text-sm font-medium">{issue.issue_type || issue.issueType || "기타"}</p>
</div>
</div>
2025-10-22 13:40:15 +09:00
<p className="text-muted-foreground mb-2 text-xs">
: {issue.customer_name || issue.customerName || "-"}
</p>
2025-10-22 13:40:15 +09:00
<p className="text-muted-foreground line-clamp-2 text-xs">{issue.description || "설명 없음"}</p>
{(issue.created_at || issue.createdAt) && (
2025-10-22 13:40:15 +09:00
<p className="text-muted-foreground mt-2 text-xs">
{new Date(issue.created_at || issue.createdAt || "").toLocaleDateString("ko-KR")}
</p>
)}
</div>
))
)}
</div>
</div>
);
}