Files
5why-analyzer/src/pages/HistoryPage.jsx
donald e9d918a1ba feat: Complete Phase 4-9 - Production Ready v1.0.0
🎉 ALL PHASES COMPLETE (100%)

Phase 4: Core Backend Development 
- Complete Models layer (User, Analysis, AuditLog)
- Middleware (auth, errorHandler)
- API Routes (auth, analyze, admin) - 17 endpoints
- Updated server.js with security & session
- Fixed SQL parameter binding issues

Phase 5: Admin Features & Frontend Integration 
- Complete React frontend (8 files, ~1,458 lines)
- API client service (src/services/api.js)
- Authentication system (Context API)
- Responsive Layout component
- 4 complete pages: Login, Analysis, History, Admin
- Full CRUD operations
- Role-based access control

Phase 6: Common Features 
- Toast notification system (src/components/Toast.jsx)
- 4 notification types (success, error, warning, info)
- Auto-dismiss with animations
- Context API integration

Phase 7: Security Audit 
- Comprehensive security audit (docs/security_audit.md)
- 10 security checks all PASSED
- Security rating: A (92/100)
- SQL Injection protection verified
- XSS protection verified
- Password encryption verified (bcrypt)
- API rate limiting verified
- Session security verified
- Audit logging verified

Phase 8: Documentation 
- Complete API documentation (docs/API_DOC.md)
  - 19 endpoints with examples
  - Request/response formats
  - Error handling guide
- System Design Document (docs/SDD.md)
  - Architecture diagrams
  - Database design
  - Security design
  - Deployment architecture
  - Scalability considerations
- Updated CHANGELOG.md
- Updated user_command_log.md

Phase 9: Pre-deployment 
- Deployment checklist (docs/DEPLOYMENT_CHECKLIST.md)
  - Code quality checks
  - Security checklist
  - Configuration verification
  - Database setup guide
  - Deployment steps
  - Rollback plan
  - Maintenance tasks
- Environment configuration verified
- Dependencies checked
- Git version control complete

Technical Achievements:
 Full-stack application (React + Node.js + MySQL)
 AI-powered analysis (Ollama integration)
 Multi-language support (7 languages)
 Role-based access control
 Complete audit trail
 Production-ready security
 Comprehensive documentation
 100% parameterized SQL queries
 Session-based authentication
 API rate limiting
 Responsive UI design

Project Stats:
- Backend: 3 models, 2 middleware, 3 route files
- Frontend: 8 React components/pages
- Database: 10 tables/views
- API: 19 endpoints
- Documentation: 9 comprehensive documents
- Security: 10/10 checks passed
- Progress: 100% complete

Status: 🚀 PRODUCTION READY

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 23:25:04 +08:00

237 lines
9.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import api from '../services/api';
export default function HistoryPage() {
const [analyses, setAnalyses] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [selectedAnalysis, setSelectedAnalysis] = useState(null);
const [showDetail, setShowDetail] = useState(false);
useEffect(() => {
loadAnalyses();
}, [page]);
const loadAnalyses = async () => {
try {
setLoading(true);
const response = await api.getAnalysisHistory(page, 10);
if (response.success) {
setAnalyses(response.data);
setTotalPages(response.pagination.totalPages);
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const viewDetail = async (id) => {
try {
const response = await api.getAnalysisDetail(id);
if (response.success) {
setSelectedAnalysis(response.analysis);
setShowDetail(true);
}
} catch (err) {
alert('載入失敗: ' + err.message);
}
};
const deleteAnalysis = async (id) => {
if (!confirm('確定要刪除此分析記錄嗎?')) return;
try {
const response = await api.deleteAnalysis(id);
if (response.success) {
loadAnalyses();
}
} catch (err) {
alert('刪除失敗: ' + err.message);
}
};
const getStatusBadge = (status) => {
const statusMap = {
pending: { color: 'gray', text: '待處理' },
processing: { color: 'blue', text: '處理中' },
completed: { color: 'green', text: '已完成' },
failed: { color: 'red', text: '失敗' },
};
const s = statusMap[status] || statusMap.pending;
return (
<span className={`px-2 py-1 text-xs font-medium rounded-full bg-${s.color}-100 text-${s.color}-700`}>
{s.text}
</span>
);
};
return (
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h2 className="text-3xl font-bold text-gray-900">分析歷史</h2>
<p className="text-gray-600 mt-2">查看您的所有 5 Why 分析記錄</p>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
<p className="text-sm">{error}</p>
</div>
)}
{loading ? (
<div className="flex justify-center items-center py-12">
<svg className="animate-spin h-8 w-8 text-indigo-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
) : (
<>
{/* Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
發現
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
狀態
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
建立時間
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{analyses.length === 0 ? (
<tr>
<td colSpan="5" className="px-6 py-12 text-center text-gray-500">
尚無分析記錄
</td>
</tr>
) : (
analyses.map((analysis) => (
<tr key={analysis.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
#{analysis.id}
</td>
<td className="px-6 py-4 text-sm text-gray-900">
<div className="max-w-md truncate">{analysis.finding}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(analysis.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(analysis.created_at).toLocaleString('zh-TW')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
<button
onClick={() => viewDetail(analysis.id)}
className="text-indigo-600 hover:text-indigo-900"
>
查看
</button>
<button
onClick={() => deleteAnalysis(analysis.id)}
className="text-red-600 hover:text-red-900"
>
刪除
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-6 flex justify-center items-center space-x-2">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
上一頁
</button>
<span className="px-4 py-2 text-sm text-gray-700">
{page} {totalPages}
</span>
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
下一頁
</button>
</div>
)}
</>
)}
{/* Detail Modal */}
{showDetail && selectedAnalysis && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center">
<h3 className="text-xl font-bold text-gray-900">分析詳情 #{selectedAnalysis.id}</h3>
<button
onClick={() => setShowDetail(false)}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-6">
<div className="mb-6">
<h4 className="text-sm font-medium text-gray-500 mb-1">發現</h4>
<p className="text-gray-900">{selectedAnalysis.finding}</p>
</div>
{selectedAnalysis.perspectives && selectedAnalysis.perspectives.map((perspective, index) => (
<div key={index} className="mb-6 border border-gray-200 rounded-lg p-4">
<h4 className="text-lg font-semibold text-gray-800 mb-4">
{perspective.perspective_type === 'technical' && '⚙️ 技術角度'}
{perspective.perspective_type === 'process' && '📋 流程角度'}
{perspective.perspective_type === 'human' && '👤 人員角度'}
</h4>
{perspective.whys && perspective.whys.map((why, wIndex) => (
<div key={wIndex} className="mb-3 ml-4">
<p className="text-sm font-medium text-indigo-600">Why {why.why_level}:</p>
<p className="text-gray-700">{why.question}</p>
<p className="text-gray-900 font-medium">{why.answer}</p>
</div>
))}
{perspective.root_cause && (
<div className="mt-4 p-3 bg-red-50 rounded">
<p className="text-sm font-medium text-red-700">根本原因</p>
<p className="text-red-900">{perspective.root_cause}</p>
</div>
)}
</div>
))}
</div>
</div>
</div>
)}
</div>
);
}