実装完了
Backend(Django) backend/apps/mail/serializers.py MailEmailListSerializer を新規追加(フロントエンド向けメール一覧用) feedback_token フィールドを含む(フィードバックリンク表示用) backend/apps/mail/views.py MailEmailCreateView → MailEmailView に変更(GET+POST を統合) GET /api/mail/emails/ : JWT認証でメール履歴取得(最新100件、account/verdict フィルター対応) POST /api/mail/emails/ : APIキー認証でWindmillからのメール記録(既存動作を維持) get_permissions() でメソッドごとに認証方法を切替 MailStatsView を新規追加 GET /api/mail/stats/ : 今日の処理件数、LINE通知数、フィードバック待ち、ルール数を返す backend/apps/mail/urls.py emails/ → MailEmailView(GET+POST) stats/ → MailStatsView を追加 Frontend(Next.js) frontend/src/app/mail/history/page.tsx (新規作成) メール処理履歴の一覧テーブル アカウント・LLM判定でフィルタリング可能 LLM判定・フィードバック状態をバッジで表示 フィードバックトークンがあれば「回答」リンクを表示 frontend/src/app/dashboard/page.tsx (再設計) 2カラムのモジュールカード形式に変更 作付け計画カード: 年度セレクタ、集計数値、作物別集計、クイックアクセス メール通知カード: 今日の処理件数、LINE通知数、フィードバック待ち、ルール数、メール履歴・ルール管理ボタン
This commit is contained in:
199
frontend/src/app/mail/history/page.tsx
Normal file
199
frontend/src/app/mail/history/page.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { api } from '@/lib/api';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import { Mail, Loader2, ExternalLink, CheckCircle, XCircle, Clock, Shield } from 'lucide-react';
|
||||
|
||||
interface MailEmail {
|
||||
id: number;
|
||||
account: string;
|
||||
sender_email: string;
|
||||
sender_domain: string;
|
||||
subject: string;
|
||||
received_at: string;
|
||||
llm_verdict: 'important' | 'not_important';
|
||||
notified_at: string | null;
|
||||
feedback: 'important' | 'not_important' | 'never_notify' | 'always_notify' | null;
|
||||
feedback_at: string | null;
|
||||
feedback_token: string | null;
|
||||
}
|
||||
|
||||
const FEEDBACK_LABELS: Record<string, string> = {
|
||||
important: '重要だった',
|
||||
not_important: '普通のメール',
|
||||
never_notify: '通知しない',
|
||||
always_notify: '常に通知',
|
||||
};
|
||||
|
||||
const ACCOUNT_LABELS: Record<string, string> = {
|
||||
gmail: 'Gmail',
|
||||
hotmail: 'Hotmail',
|
||||
xserver: 'Xserver',
|
||||
};
|
||||
|
||||
export default function MailHistoryPage() {
|
||||
const router = useRouter();
|
||||
const [emails, setEmails] = useState<MailEmail[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filterAccount, setFilterAccount] = useState('');
|
||||
const [filterVerdict, setFilterVerdict] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEmails = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filterAccount) params.set('account', filterAccount);
|
||||
if (filterVerdict) params.set('verdict', filterVerdict);
|
||||
const res = await api.get(`/mail/emails/?${params.toString()}`);
|
||||
setEmails(res.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch emails:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchEmails();
|
||||
}, [filterAccount, filterVerdict]);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('ja-JP', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Navbar />
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* ヘッダー */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail className="h-6 w-6 text-blue-600" />
|
||||
<h1 className="text-2xl font-bold text-gray-900">メール処理履歴</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/mail/rules')}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors text-gray-700"
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
ルール管理
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* フィルター */}
|
||||
<div className="bg-white rounded-lg shadow p-4 mb-4 flex gap-3">
|
||||
<select
|
||||
value={filterAccount}
|
||||
onChange={(e) => setFilterAccount(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="">全アカウント</option>
|
||||
<option value="gmail">Gmail</option>
|
||||
<option value="hotmail">Hotmail</option>
|
||||
<option value="xserver">Xserver</option>
|
||||
</select>
|
||||
<select
|
||||
value={filterVerdict}
|
||||
onChange={(e) => setFilterVerdict(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="">全判定</option>
|
||||
<option value="important">重要</option>
|
||||
<option value="not_important">通常</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* テーブル */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-16">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : emails.length === 0 ? (
|
||||
<div className="text-center py-16 text-gray-500">処理済みメールはありません</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 text-gray-500 font-medium whitespace-nowrap">受信日時</th>
|
||||
<th className="text-left px-4 py-3 text-gray-500 font-medium">送信者</th>
|
||||
<th className="text-left px-4 py-3 text-gray-500 font-medium">件名</th>
|
||||
<th className="text-center px-4 py-3 text-gray-500 font-medium whitespace-nowrap">LLM判定</th>
|
||||
<th className="text-center px-4 py-3 text-gray-500 font-medium whitespace-nowrap">フィードバック</th>
|
||||
<th className="text-center px-4 py-3 text-gray-500 font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{emails.map((email) => (
|
||||
<tr key={email.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-gray-600 whitespace-nowrap">
|
||||
<div>{formatDate(email.received_at)}</div>
|
||||
<div className="text-xs text-gray-400">{ACCOUNT_LABELS[email.account] ?? email.account}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-gray-900 text-xs truncate max-w-[180px]">{email.sender_email}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-900 max-w-xs">
|
||||
<div className="truncate">{email.subject}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{email.llm_verdict === 'important' ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-red-50 text-red-700 text-xs font-medium">
|
||||
<CheckCircle className="h-3 w-3" />重要
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-gray-100 text-gray-500 text-xs">
|
||||
<XCircle className="h-3 w-3" />通常
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{email.feedback ? (
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
email.feedback === 'important' ? 'bg-green-50 text-green-700' :
|
||||
email.feedback === 'not_important' ? 'bg-gray-100 text-gray-600' :
|
||||
email.feedback === 'never_notify' ? 'bg-orange-50 text-orange-700' :
|
||||
'bg-blue-50 text-blue-700'
|
||||
}`}>
|
||||
{FEEDBACK_LABELS[email.feedback] ?? email.feedback}
|
||||
</span>
|
||||
) : email.llm_verdict === 'important' ? (
|
||||
<span className="inline-flex items-center gap-1 text-amber-500 text-xs">
|
||||
<Clock className="h-3 w-3" />未回答
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400 text-xs">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{email.feedback_token && (
|
||||
<a
|
||||
href={`/mail/feedback/${email.feedback_token}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 text-xs"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
回答
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="px-4 py-3 text-xs text-gray-400 border-t border-gray-100">
|
||||
最新100件を表示
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user