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

262 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";
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();
// 자동 새로고침 (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(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();
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-warning/10 text-warning dark:bg-warning/20 dark:text-warning";
} else if (priorityLower.includes("낮음") || priorityLower.includes("low")) {
return "bg-success/10 text-success dark:bg-success/20 dark:text-success";
}
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-success/10 text-success dark:bg-success/20 dark:text-success";
}
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="text-muted-foreground mt-2 text-sm"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-destructive text-center">
<p className="text-sm"> {error}</p>
<button
onClick={loadData}
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">
<div className="text-muted-foreground text-center">
<p className="text-sm"> </p>
</div>
</div>
);
}
return (
<div className="bg-background flex h-full flex-col overflow-hidden p-4">
{/* 헤더 */}
<div className="mb-4 flex items-center justify-between">
<h3 className="text-foreground text-lg font-semibold"> /</h3>
<button
onClick={loadData}
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-warning/10 text-warning"
: "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-success/10 text-success"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
>
</button>
</div>
{/* 총 건수 */}
<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 ? (
<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}
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">
<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-foreground text-sm font-medium">{issue.issue_type || issue.issueType || "기타"}</p>
</div>
</div>
<p className="text-muted-foreground mb-2 text-xs">
: {issue.customer_name || issue.customerName || "-"}
</p>
<p className="text-muted-foreground line-clamp-2 text-xs">{issue.description || "설명 없음"}</p>
{(issue.created_at || issue.createdAt) && (
<p className="text-muted-foreground mt-2 text-xs">
{new Date(issue.created_at || issue.createdAt || "").toLocaleDateString("ko-KR")}
</p>
)}
</div>
))
)}
</div>
</div>
);
}