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

261 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } from "react";
import { DashboardElement } from "@/components/admin/dashboard/types";
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();
// 자동 새로고침 (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");
const response = await fetch("/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();
if (result.success && result.data?.rows) {
setIssues(result.data.rows);
}
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
} finally {
setLoading(false);
}
};
const getPriorityBadge = (priority: string) => {
const priorityLower = priority?.toLowerCase() || "";
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() || "";
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";
};
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" />
<p className="mt-2 text-sm text-muted-foreground"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-destructive">
<p className="text-sm"> {error}</p>
<button
onClick={loadData}
className="mt-2 rounded-md bg-destructive/10 px-3 py-1 text-xs hover:bg-destructive/20"
>
</button>
</div>
</div>
);
}
if (!element?.dataSource?.query) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-muted-foreground">
<p className="text-sm"> </p>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col overflow-hidden bg-background p-4">
{/* 헤더 */}
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground"> /</h3>
<button
onClick={loadData}
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
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>
{/* 총 건수 */}
<div className="mb-3 text-sm text-muted-foreground">
<span className="font-semibold text-foreground">{filteredIssues.length}</span>
</div>
{/* 이슈 리스트 */}
<div className="flex-1 space-y-2 overflow-auto">
{filteredIssues.length === 0 ? (
<div className="flex h-full items-center justify-center text-center text-muted-foreground">
<p> </p>
</div>
) : (
filteredIssues.map((issue, index) => (
<div
key={issue.id || index}
className="rounded-lg border border-border bg-card 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">
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${getPriorityBadge(issue.priority || "")}`}>
{issue.priority || "보통"}
</span>
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${getStatusBadge(issue.status || "")}`}>
{issue.status || "처리중"}
</span>
</div>
<p className="text-sm font-medium text-foreground">
{issue.issue_type || issue.issueType || "기타"}
</p>
</div>
</div>
<p className="mb-2 text-xs text-muted-foreground">
: {issue.customer_name || issue.customerName || "-"}
</p>
<p className="text-xs text-muted-foreground line-clamp-2">
{issue.description || "설명 없음"}
</p>
{(issue.created_at || issue.createdAt) && (
<p className="mt-2 text-xs text-muted-foreground">
{new Date(issue.created_at || issue.createdAt || "").toLocaleDateString("ko-KR")}
</p>
)}
</div>
))
)}
</div>
</div>
);
}