feat: implement debug logging cleanup and i18n coverage proposals

## cleanup-debug-logging
- Create environment-aware logger utility (logger.ts)
- Replace 60+ console.log/error statements across 28 files
- Production: only warn/error logs visible
- Development: all log levels with prefixes

Updated files:
- Contexts: NotificationContext, ProjectSyncContext, AuthContext
- Components: GanttChart, CalendarView, ErrorBoundary, and 11 others
- Pages: Tasks, Projects, Dashboard, and 7 others
- Services: api.ts

## complete-i18n-coverage
- WeeklyReportPreview: all strings translated, dynamic locale
- ReportHistory: all strings translated, dynamic locale
- AuditPage: detail modal and verification modal translated
- WorkloadPage: error message translated

Locale files updated:
- en/common.json, zh-TW/common.json: reports section
- en/audit.json, zh-TW/audit.json: modal sections
- en/workload.json, zh-TW/workload.json: errors section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-13 21:37:29 +08:00
parent ed8d30e9bd
commit 4b7e523f84
37 changed files with 430 additions and 207 deletions

View File

@@ -52,5 +52,39 @@
"empty": { "empty": {
"title": "No Audit Records", "title": "No Audit Records",
"description": "No audit records match the current filters" "description": "No audit records match the current filters"
},
"modal": {
"details": {
"title": "Audit Log Details",
"eventType": "Event Type:",
"action": "Action:",
"resource": "Resource:",
"user": "User:",
"ipAddress": "IP Address:",
"sensitivity": "Sensitivity:",
"time": "Time:",
"changes": "Changes",
"field": "Field",
"oldValue": "Old Value",
"newValue": "New Value",
"checksum": "Checksum:",
"na": "N/A",
"system": "System"
},
"verification": {
"title": "Integrity Verification",
"verifying": "Verifying audit log integrity...",
"verifyingHint": "This may take a moment depending on the number of records.",
"failed": "Verification Failed",
"statusSuccess": "Integrity Verified",
"statusFailed": "Integrity Issues Detected",
"successDescription": "All audit records have valid checksums and have not been tampered with.",
"failedDescription": "Some audit records have invalid checksums, indicating potential tampering or corruption.",
"totalChecked": "Total Checked",
"valid": "Valid",
"invalid": "Invalid",
"invalidRecords": "Invalid Records",
"invalidRecordsDescription": "The following record IDs failed integrity verification:"
}
} }
} }

View File

@@ -21,7 +21,8 @@
"remove": "Remove", "remove": "Remove",
"view": "View", "view": "View",
"download": "Download", "download": "Download",
"upload": "Upload" "upload": "Upload",
"retry": "Retry"
}, },
"labels": { "labels": {
"loading": "Loading...", "loading": "Loading...",
@@ -135,5 +136,62 @@
"maxFileSize": "Maximum file size: {{size}}", "maxFileSize": "Maximum file size: {{size}}",
"uploading": "Uploading {{filename}} ({{current}}/{{total}})...", "uploading": "Uploading {{filename}} ({{current}}/{{total}})...",
"uploadFailed": "Upload failed" "uploadFailed": "Upload failed"
},
"reports": {
"weeklyPreview": {
"title": "Weekly Report Preview",
"generateNow": "Generate Now",
"generating": "Generating...",
"loading": "Loading report preview...",
"noData": "No report data available",
"noProjects": "No projects found",
"projects": "Projects",
"retry": "Retry",
"completedShort": "completed",
"done": "done",
"inProgressLower": "in progress",
"overdueLower": "overdue",
"blockedLower": "blocked",
"nextWeekLower": "next week",
"unassigned": "Unassigned",
"due": "Due",
"since": "Since",
"noReasonProvided": "No reason provided",
"daysOverdue": "{{count}} day overdue",
"daysOverdue_other": "{{count}} days overdue",
"status": {
"completed": "Completed",
"inProgress": "In Progress",
"overdue": "Overdue",
"blocked": "Blocked",
"nextWeek": "Next Week",
"total": "Total"
},
"errors": {
"loadFailed": "Failed to load report preview",
"generateFailed": "Failed to generate report"
},
"messages": {
"generateSuccess": "Report generated and notification sent!"
}
},
"history": {
"title": "Report History",
"loading": "Loading history...",
"retry": "Retry",
"empty": "No report history found. Reports are generated every Friday at 16:00.",
"totalReports": "{{count}} report",
"totalReports_other": "{{count}} reports",
"completed": "Completed",
"inProgress": "In Progress",
"overdue": "Overdue",
"status": {
"sent": "sent",
"failed": "failed"
},
"errors": {
"loadFailed": "Failed to load report history"
}
}
} }
} }

View File

@@ -68,5 +68,8 @@
"normal": "Normal: < 80%", "normal": "Normal: < 80%",
"warning": "Warning: 80% - 99%", "warning": "Warning: 80% - 99%",
"overloaded": "Overloaded: ≥ 100%" "overloaded": "Overloaded: ≥ 100%"
},
"errors": {
"loadFailed": "Failed to load workload data. Please try again."
} }
} }

View File

@@ -52,5 +52,39 @@
"empty": { "empty": {
"title": "沒有稽核記錄", "title": "沒有稽核記錄",
"description": "目前沒有符合條件的稽核記錄" "description": "目前沒有符合條件的稽核記錄"
},
"modal": {
"details": {
"title": "稽核日誌詳情",
"eventType": "事件類型:",
"action": "操作:",
"resource": "資源:",
"user": "使用者:",
"ipAddress": "IP 位址:",
"sensitivity": "敏感度:",
"time": "時間:",
"changes": "變更內容",
"field": "欄位",
"oldValue": "舊值",
"newValue": "新值",
"checksum": "校驗碼:",
"na": "無",
"system": "系統"
},
"verification": {
"title": "完整性驗證",
"verifying": "正在驗證稽核日誌完整性...",
"verifyingHint": "根據記錄數量,這可能需要一些時間。",
"failed": "驗證失敗",
"statusSuccess": "完整性已驗證",
"statusFailed": "偵測到完整性問題",
"successDescription": "所有稽核記錄都具有有效的校驗碼,未被竄改。",
"failedDescription": "部分稽核記錄的校驗碼無效,表示可能存在竄改或損壞。",
"totalChecked": "已檢查總數",
"valid": "有效",
"invalid": "無效",
"invalidRecords": "無效記錄",
"invalidRecordsDescription": "以下記錄 ID 未通過完整性驗證:"
}
} }
} }

View File

@@ -21,7 +21,8 @@
"remove": "移除", "remove": "移除",
"view": "檢視", "view": "檢視",
"download": "下載", "download": "下載",
"upload": "上傳" "upload": "上傳",
"retry": "重試"
}, },
"labels": { "labels": {
"loading": "載入中...", "loading": "載入中...",
@@ -135,5 +136,60 @@
"maxFileSize": "檔案大小上限:{{size}}", "maxFileSize": "檔案大小上限:{{size}}",
"uploading": "正在上傳 {{filename}} ({{current}}/{{total}})...", "uploading": "正在上傳 {{filename}} ({{current}}/{{total}})...",
"uploadFailed": "上傳失敗" "uploadFailed": "上傳失敗"
},
"reports": {
"weeklyPreview": {
"title": "週報預覽",
"generateNow": "立即產生",
"generating": "產生中...",
"loading": "載入報告預覽中...",
"noData": "無可用的報告資料",
"noProjects": "找不到專案",
"projects": "專案",
"retry": "重試",
"completedShort": "已完成",
"done": "完成",
"inProgressLower": "進行中",
"overdueLower": "逾期",
"blockedLower": "受阻",
"nextWeekLower": "下週",
"unassigned": "未指派",
"due": "截止",
"since": "自",
"noReasonProvided": "未提供原因",
"daysOverdue": "逾期 {{count}} 天",
"status": {
"completed": "已完成",
"inProgress": "進行中",
"overdue": "逾期",
"blocked": "受阻",
"nextWeek": "下週",
"total": "總計"
},
"errors": {
"loadFailed": "載入報告預覽失敗",
"generateFailed": "產生報告失敗"
},
"messages": {
"generateSuccess": "報告已產生並發送通知!"
}
},
"history": {
"title": "報告歷史",
"loading": "載入歷史中...",
"retry": "重試",
"empty": "找不到報告歷史記錄。報告會在每週五 16:00 自動產生。",
"totalReports": "{{count}} 份報告",
"completed": "已完成",
"inProgress": "進行中",
"overdue": "逾期",
"status": {
"sent": "已發送",
"failed": "失敗"
},
"errors": {
"loadFailed": "載入報告歷史失敗"
}
}
} }
} }

View File

@@ -68,5 +68,8 @@
"normal": "正常:< 80%", "normal": "正常:< 80%",
"warning": "警告80% - 99%", "warning": "警告80% - 99%",
"overloaded": "超載:≥ 100%" "overloaded": "超載:≥ 100%"
},
"errors": {
"loadFailed": "載入工作負載資料失敗,請再試一次。"
} }
} }

View File

@@ -4,6 +4,7 @@ import { AttachmentVersionHistory } from './AttachmentVersionHistory'
import { ConfirmModal } from './ConfirmModal' import { ConfirmModal } from './ConfirmModal'
import { useToast } from '../contexts/ToastContext' import { useToast } from '../contexts/ToastContext'
import { SkeletonList } from './Skeleton' import { SkeletonList } from './Skeleton'
import { logger } from '../utils/logger'
interface AttachmentListProps { interface AttachmentListProps {
taskId: string taskId: string
@@ -30,7 +31,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
const response = await attachmentService.listAttachments(taskId) const response = await attachmentService.listAttachments(taskId)
setAttachments(response.attachments) setAttachments(response.attachments)
} catch (error) { } catch (error) {
console.error('Failed to load attachments:', error) logger.error('Failed to load attachments:', error)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -44,7 +45,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
try { try {
await attachmentService.downloadAttachment(attachment.id) await attachmentService.downloadAttachment(attachment.id)
} catch (error) { } catch (error) {
console.error('Failed to download attachment:', error) logger.error('Failed to download attachment:', error)
showToast('Failed to download file', 'error') showToast('Failed to download file', 'error')
} }
} }
@@ -61,7 +62,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
onRefresh?.() onRefresh?.()
showToast('File deleted successfully', 'success') showToast('File deleted successfully', 'success')
} catch (error) { } catch (error) {
console.error('Failed to delete attachment:', error) logger.error('Failed to delete attachment:', error)
showToast('Failed to delete file', 'error') showToast('Failed to delete file', 'error')
} finally { } finally {
setDeleting(null) setDeleting(null)

View File

@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, DragEvent, ChangeEvent } from 'react' import { useState, useRef, useEffect, DragEvent, ChangeEvent } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { attachmentService } from '../services/attachments' import { attachmentService } from '../services/attachments'
import { logger } from '../utils/logger'
// Spinner animation keyframes - injected once via useEffect // Spinner animation keyframes - injected once via useEffect
const SPINNER_KEYFRAMES_ID = 'attachment-upload-spinner-keyframes' const SPINNER_KEYFRAMES_ID = 'attachment-upload-spinner-keyframes'
@@ -93,7 +94,7 @@ export function AttachmentUpload({ taskId, onUploadComplete }: AttachmentUploadP
setUploadProgress(null) setUploadProgress(null)
onUploadComplete?.() onUploadComplete?.()
} catch (err: unknown) { } catch (err: unknown) {
console.error('Upload failed:', err) logger.error('Upload failed:', err)
const errorMessage = err instanceof Error ? err.message : t('attachments.uploadFailed') const errorMessage = err instanceof Error ? err.message : t('attachments.uploadFailed')
setError(errorMessage) setError(errorMessage)
} finally { } finally {

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { attachmentService, AttachmentVersion, VersionHistoryResponse } from '../services/attachments' import { attachmentService, AttachmentVersion, VersionHistoryResponse } from '../services/attachments'
import { logger } from '../utils/logger'
interface AttachmentVersionHistoryProps { interface AttachmentVersionHistoryProps {
attachmentId: string attachmentId: string
@@ -50,7 +51,7 @@ export function AttachmentVersionHistory({
const response: VersionHistoryResponse = await attachmentService.getVersionHistory(attachmentId) const response: VersionHistoryResponse = await attachmentService.getVersionHistory(attachmentId)
setVersions(response.versions) setVersions(response.versions)
} catch (err) { } catch (err) {
console.error('Failed to load version history:', err) logger.error('Failed to load version history:', err)
setError('Failed to load version history') setError('Failed to load version history')
} finally { } finally {
setLoading(false) setLoading(false)
@@ -72,7 +73,7 @@ export function AttachmentVersionHistory({
onRestore() onRestore()
onClose() onClose()
} catch (err) { } catch (err) {
console.error('Failed to restore version:', err) logger.error('Failed to restore version:', err)
setError('Failed to restore version. Please try again.') setError('Failed to restore version. Please try again.')
} finally { } finally {
setRestoring(null) setRestoring(null)
@@ -83,7 +84,7 @@ export function AttachmentVersionHistory({
try { try {
await attachmentService.downloadAttachment(attachmentId, version) await attachmentService.downloadAttachment(attachmentId, version)
} catch (err) { } catch (err) {
console.error('Failed to download version:', err) logger.error('Failed to download version:', err)
setError('Failed to download version') setError('Failed to download version')
} }
} }

View File

@@ -7,6 +7,7 @@ import interactionPlugin from '@fullcalendar/interaction'
import { EventClickArg, EventDropArg, DatesSetArg } from '@fullcalendar/core' import { EventClickArg, EventDropArg, DatesSetArg } from '@fullcalendar/core'
import api from '../services/api' import api from '../services/api'
import { Skeleton } from './Skeleton' import { Skeleton } from './Skeleton'
import { logger } from '../utils/logger'
interface Task { interface Task {
id: string id: string
@@ -109,7 +110,7 @@ export function CalendarView({
Array.from(uniqueAssignees.entries()).map(([id, name]) => ({ id, name })) Array.from(uniqueAssignees.entries()).map(([id, name]) => ({ id, name }))
) )
} catch (err) { } catch (err) {
console.error('Failed to load assignees:', err) logger.error('Failed to load assignees:', err)
} }
} }
@@ -169,7 +170,7 @@ export function CalendarView({
setEvents(calendarEvents) setEvents(calendarEvents)
} catch (err) { } catch (err) {
console.error('Failed to load tasks:', err) logger.error('Failed to load tasks:', err)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -277,7 +278,7 @@ export function CalendarView({
onTaskUpdate() onTaskUpdate()
} }
} }
console.error('Failed to update task date:', err) logger.error('Failed to update task date:', err)
// Rollback on error // Rollback on error
dropInfo.revert() dropInfo.revert()
} }

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import { customFieldsApi, CustomField, FieldType } from '../services/customFields' import { customFieldsApi, CustomField, FieldType } from '../services/customFields'
import { CustomFieldEditor } from './CustomFieldEditor' import { CustomFieldEditor } from './CustomFieldEditor'
import { useToast } from '../contexts/ToastContext' import { useToast } from '../contexts/ToastContext'
import { logger } from '../utils/logger'
interface CustomFieldListProps { interface CustomFieldListProps {
projectId: string projectId: string
@@ -39,7 +40,7 @@ export function CustomFieldList({ projectId }: CustomFieldListProps) {
const response = await customFieldsApi.getCustomFields(projectId) const response = await customFieldsApi.getCustomFields(projectId)
setFields(response.fields) setFields(response.fields)
} catch (err) { } catch (err) {
console.error('Failed to load custom fields:', err) logger.error('Failed to load custom fields:', err)
setError(t('customFields.loadError')) setError(t('customFields.loadError'))
} finally { } finally {
setLoading(false) setLoading(false)

View File

@@ -1,4 +1,5 @@
import React, { Component, ErrorInfo, ReactNode } from 'react' import React, { Component, ErrorInfo, ReactNode } from 'react'
import { logger } from '../utils/logger'
// Error logging service - can be extended to send to external service // Error logging service - can be extended to send to external service
export interface ErrorLog { export interface ErrorLog {
@@ -25,13 +26,12 @@ export function logError(error: Error, errorInfo: ErrorInfo): ErrorLog {
errorLogs.push(log) errorLogs.push(log)
// Log to console for debugging // Log to console for debugging (always logged as these are critical errors)
console.group('ErrorBoundary caught an error') logger.error('ErrorBoundary caught an error')
console.error('Error:', error) logger.error('Error:', error)
console.error('Component Stack:', errorInfo.componentStack) logger.error('Component Stack:', errorInfo.componentStack)
console.error('Timestamp:', log.timestamp.toISOString()) logger.error('Timestamp:', log.timestamp.toISOString())
console.error('URL:', log.url) logger.error('URL:', log.url)
console.groupEnd()
// In production, could send to error tracking service // In production, could send to error tracking service
// sendToErrorTrackingService(log) // sendToErrorTrackingService(log)

View File

@@ -5,6 +5,7 @@ import api from '../services/api'
import { dependenciesApi, TaskDependency, DependencyType } from '../services/dependencies' import { dependenciesApi, TaskDependency, DependencyType } from '../services/dependencies'
import { CircularDependencyError, parseCircularError } from './CircularDependencyError' import { CircularDependencyError, parseCircularError } from './CircularDependencyError'
import { escapeHtml } from '../utils/escapeHtml' import { escapeHtml } from '../utils/escapeHtml'
import { logger } from '../utils/logger'
interface CycleDetails { interface CycleDetails {
cycle: string[] cycle: string[]
@@ -91,7 +92,7 @@ export function GanttChart({
const deps = await dependenciesApi.getProjectDependencies(projectId) const deps = await dependenciesApi.getProjectDependencies(projectId)
setDependencies(deps) setDependencies(deps)
} catch (err) { } catch (err) {
console.error('Failed to load dependencies:', err) logger.error('Failed to load dependencies:', err)
} }
} }
@@ -283,7 +284,7 @@ export function GanttChart({
await api.patch(`/tasks/${taskId}`, payload) await api.patch(`/tasks/${taskId}`, payload)
onTaskUpdate() onTaskUpdate()
} catch (err: unknown) { } catch (err: unknown) {
console.error('Failed to update task dates:', err) logger.error('Failed to update task dates:', err)
const error = err as { response?: { status?: number; data?: { detail?: string | { message?: string } } } } const error = err as { response?: { status?: number; data?: { detail?: string | { message?: string } } } }
// Handle 409 Conflict - version mismatch // Handle 409 Conflict - version mismatch
if (error.response?.status === 409) { if (error.response?.status === 409) {
@@ -306,8 +307,8 @@ export function GanttChart({
// Handle progress change // Handle progress change
const handleProgressChange = async (taskId: string, progress: number) => { const handleProgressChange = async (taskId: string, progress: number) => {
// Progress changes could update task status in the future // Progress changes could update task status in the future
// For now, just log it // For now, just log it for debugging
console.log(`Task ${taskId} progress changed to ${progress}%`) logger.debug(`Task ${taskId} progress changed to ${progress}%`)
} }
// Add dependency // Add dependency
@@ -328,7 +329,7 @@ export function GanttChart({
setSelectedPredecessor('') setSelectedPredecessor('')
setSelectedDependencyType('FS') setSelectedDependencyType('FS')
} catch (err: unknown) { } catch (err: unknown) {
console.error('Failed to add dependency:', err) logger.error('Failed to add dependency:', err)
const error = err as { response?: { data?: { detail?: unknown } } } const error = err as { response?: { data?: { detail?: unknown } } }
const errorDetail = error.response?.data?.detail const errorDetail = error.response?.data?.detail
@@ -353,7 +354,7 @@ export function GanttChart({
await dependenciesApi.removeDependency(dependencyId) await dependenciesApi.removeDependency(dependencyId)
await loadDependencies() await loadDependencies()
} catch (err) { } catch (err) {
console.error('Failed to remove dependency:', err) logger.error('Failed to remove dependency:', err)
setError('Failed to remove dependency') setError('Failed to remove dependency')
} }
} }

View File

@@ -5,6 +5,7 @@ import { useToast } from '../contexts/ToastContext'
import { Skeleton } from './Skeleton' import { Skeleton } from './Skeleton'
import { ConfirmModal } from './ConfirmModal' import { ConfirmModal } from './ConfirmModal'
import { AddMemberModal } from './AddMemberModal' import { AddMemberModal } from './AddMemberModal'
import { logger } from '../utils/logger'
interface ProjectMemberListProps { interface ProjectMemberListProps {
projectId: string projectId: string
@@ -28,7 +29,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
const response = await projectMembersApi.list(projectId) const response = await projectMembersApi.list(projectId)
setMembers(response.members) setMembers(response.members)
} catch (err) { } catch (err) {
console.error('Failed to load project members:', err) logger.error('Failed to load project members:', err)
setError(t('members.loadError')) setError(t('members.loadError'))
} finally { } finally {
setLoading(false) setLoading(false)
@@ -47,7 +48,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
setIsAddModalOpen(false) setIsAddModalOpen(false)
showToast(t('members.memberAdded'), 'success') showToast(t('members.memberAdded'), 'success')
} catch (err: unknown) { } catch (err: unknown) {
console.error('Failed to add member:', err) logger.error('Failed to add member:', err)
const errorMessage = err instanceof Error ? err.message : t('members.addError') const errorMessage = err instanceof Error ? err.message : t('members.addError')
const axiosError = err as { response?: { data?: { detail?: string } } } const axiosError = err as { response?: { data?: { detail?: string } } }
if (axiosError.response?.data?.detail) { if (axiosError.response?.data?.detail) {
@@ -70,7 +71,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
setMemberToRemove(null) setMemberToRemove(null)
showToast(t('messages.memberRemoved'), 'success') showToast(t('messages.memberRemoved'), 'success')
} catch (err) { } catch (err) {
console.error('Failed to remove member:', err) logger.error('Failed to remove member:', err)
showToast(t('members.removeError'), 'error') showToast(t('members.removeError'), 'error')
} finally { } finally {
setActionLoading(false) setActionLoading(false)
@@ -94,7 +95,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
setEditingMemberId(null) setEditingMemberId(null)
showToast(t('messages.roleChanged'), 'success') showToast(t('messages.roleChanged'), 'success')
} catch (err) { } catch (err) {
console.error('Failed to update member role:', err) logger.error('Failed to update member role:', err)
showToast(t('members.roleChangeError'), 'error') showToast(t('members.roleChangeError'), 'error')
} finally { } finally {
setActionLoading(false) setActionLoading(false)

View File

@@ -1,7 +1,9 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { reportsApi, ReportHistoryItem } from '../services/reports' import { reportsApi, ReportHistoryItem } from '../services/reports'
export function ReportHistory() { export function ReportHistory() {
const { t, i18n } = useTranslation()
const [reports, setReports] = useState<ReportHistoryItem[]>([]) const [reports, setReports] = useState<ReportHistoryItem[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -15,7 +17,7 @@ export function ReportHistory() {
setTotal(data.total) setTotal(data.total)
setError(null) setError(null)
} catch { } catch {
setError('Failed to load report history') setError(t('reports.history.errors.loadFailed'))
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -26,7 +28,7 @@ export function ReportHistory() {
}, []) }, [])
const formatDate = (dateStr: string) => { const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString('zh-TW', { return new Date(dateStr).toLocaleString(i18n.language, {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
@@ -36,7 +38,7 @@ export function ReportHistory() {
} }
if (loading) { if (loading) {
return <div className="p-4 text-center text-gray-500">Loading history...</div> return <div className="p-4 text-center text-gray-500">{t('reports.history.loading')}</div>
} }
if (error) { if (error) {
@@ -44,7 +46,7 @@ export function ReportHistory() {
<div className="p-4 text-center text-red-500"> <div className="p-4 text-center text-red-500">
{error} {error}
<button onClick={fetchHistory} className="ml-2 text-blue-600 hover:underline"> <button onClick={fetchHistory} className="ml-2 text-blue-600 hover:underline">
Retry {t('reports.history.retry')}
</button> </button>
</div> </div>
) )
@@ -53,7 +55,7 @@ export function ReportHistory() {
if (reports.length === 0) { if (reports.length === 0) {
return ( return (
<div className="p-4 text-center text-gray-500"> <div className="p-4 text-center text-gray-500">
No report history found. Reports are generated every Friday at 16:00. {t('reports.history.empty')}
</div> </div>
) )
} }
@@ -61,8 +63,8 @@ export function ReportHistory() {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Report History</h3> <h3 className="text-lg font-medium">{t('reports.history.title')}</h3>
<span className="text-sm text-gray-500">{total} reports</span> <span className="text-sm text-gray-500">{t('reports.history.totalReports', { count: total })}</span>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -80,9 +82,9 @@ export function ReportHistory() {
<p className="font-medium">{formatDate(report.generated_at)}</p> <p className="font-medium">{formatDate(report.generated_at)}</p>
{report.status === 'sent' && summary && ( {report.status === 'sent' && summary && (
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
Completed: {summary.completed_count || 0} | {t('reports.history.completed')}: {summary.completed_count || 0} |
In Progress: {summary.in_progress_count || 0} | {t('reports.history.inProgress')}: {summary.in_progress_count || 0} |
Overdue: {summary.overdue_count || 0} {t('reports.history.overdue')}: {summary.overdue_count || 0}
</p> </p>
)} )}
{report.status === 'failed' && report.error_message && ( {report.status === 'failed' && report.error_message && (
@@ -94,7 +96,7 @@ export function ReportHistory() {
? 'bg-green-100 text-green-800' ? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800' : 'bg-red-100 text-red-800'
}`}> }`}>
{report.status} {t(`reports.history.status.${report.status}`)}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { auditService, AuditLog } from '../services/audit' import { auditService, AuditLog } from '../services/audit'
import { logger } from '../utils/logger'
interface ResourceHistoryProps { interface ResourceHistoryProps {
resourceType: string resourceType: string
@@ -18,7 +19,7 @@ export function ResourceHistory({ resourceType, resourceId, title = 'Change Hist
const response = await auditService.getResourceHistory(resourceType, resourceId, 10) const response = await auditService.getResourceHistory(resourceType, resourceId, 10)
setLogs(response.logs) setLogs(response.logs)
} catch (error) { } catch (error) {
console.error('Failed to load resource history:', error) logger.error('Failed to load resource history:', error)
} finally { } finally {
setLoading(false) setLoading(false)
} }

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import api from '../services/api' import api from '../services/api'
import { logger } from '../utils/logger'
interface Subtask { interface Subtask {
id: string id: string
@@ -44,7 +45,7 @@ export function SubtaskList({
setSubtasks(response.data.tasks || []) setSubtasks(response.data.tasks || [])
setError(null) setError(null)
} catch (err) { } catch (err) {
console.error('Failed to fetch subtasks:', err) logger.error('Failed to fetch subtasks:', err)
setError(t('subtasks.error.load')) setError(t('subtasks.error.load'))
} finally { } finally {
setLoading(false) setLoading(false)
@@ -71,7 +72,7 @@ export function SubtaskList({
fetchSubtasks() fetchSubtasks()
onSubtaskCreated?.() onSubtaskCreated?.()
} catch (err: unknown) { } catch (err: unknown) {
console.error('Failed to create subtask:', err) logger.error('Failed to create subtask:', err)
const axiosError = err as { response?: { data?: { detail?: string } } } const axiosError = err as { response?: { data?: { detail?: string } } }
const errorMessage = axiosError.response?.data?.detail || t('subtasks.error.create') const errorMessage = axiosError.response?.data?.detail || t('subtasks.error.create')
setError(errorMessage) setError(errorMessage)

View File

@@ -9,6 +9,7 @@ import { UserSearchResult } from '../services/collaboration'
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields' import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
import { CustomFieldInput } from './CustomFieldInput' import { CustomFieldInput } from './CustomFieldInput'
import { SkeletonList } from './Skeleton' import { SkeletonList } from './Skeleton'
import { logger } from '../utils/logger'
interface Task { interface Task {
id: string id: string
@@ -88,7 +89,7 @@ export function TaskDetailModal({
const response = await customFieldsApi.getCustomFields(task.project_id) const response = await customFieldsApi.getCustomFields(task.project_id)
setCustomFields(response.fields) setCustomFields(response.fields)
} catch (err) { } catch (err) {
console.error('Failed to load custom fields:', err) logger.error('Failed to load custom fields:', err)
} finally { } finally {
setLoadingCustomFields(false) setLoadingCustomFields(false)
} }
@@ -222,7 +223,7 @@ export function TaskDetailModal({
} }
} }
} }
console.error('Failed to update task:', err) logger.error('Failed to update task:', err)
} finally { } finally {
setSaving(false) setSaving(false)
} }

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { usersApi, UserSearchResult } from '../services/collaboration' import { usersApi, UserSearchResult } from '../services/collaboration'
import { logger } from '../utils/logger'
interface UserSelectProps { interface UserSelectProps {
value: string | null value: string | null
@@ -46,7 +47,7 @@ export function UserSelect({
const results = await usersApi.search(query) const results = await usersApi.search(query)
setUsers(results) setUsers(results)
} catch (err) { } catch (err) {
console.error('Failed to search users:', err) logger.error('Failed to search users:', err)
setUsers([]) setUsers([])
} finally { } finally {
setLoading(false) setLoading(false)

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { reportsApi, WeeklyReportContent, ProjectSummary } from '../services/reports' import { reportsApi, WeeklyReportContent, ProjectSummary } from '../services/reports'
import { useToast } from '../contexts/ToastContext' import { useToast } from '../contexts/ToastContext'
@@ -53,9 +54,11 @@ function TaskItem({ title, subtitle, highlight }: TaskItemProps) {
} }
function ProjectCard({ project }: { project: ProjectSummary }) { function ProjectCard({ project }: { project: ProjectSummary }) {
const { t, i18n } = useTranslation()
const formatDate = (dateStr: string | null) => { const formatDate = (dateStr: string | null) => {
if (!dateStr) return '' if (!dateStr) return ''
return new Date(dateStr).toLocaleDateString('zh-TW', { month: 'short', day: 'numeric' }) return new Date(dateStr).toLocaleDateString(i18n.language, { month: 'short', day: 'numeric' })
} }
return ( return (
@@ -63,78 +66,78 @@ function ProjectCard({ project }: { project: ProjectSummary }) {
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<h5 className="font-medium">{project.project_title}</h5> <h5 className="font-medium">{project.project_title}</h5>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{project.completed_count}/{project.total_tasks} completed {project.completed_count}/{project.total_tasks} {t('reports.weeklyPreview.completedShort')}
</span> </span>
</div> </div>
{/* Summary row */} {/* Summary row */}
<div className="flex flex-wrap gap-3 text-sm mb-2"> <div className="flex flex-wrap gap-3 text-sm mb-2">
<span className="text-green-600">{project.completed_count} done</span> <span className="text-green-600">{project.completed_count} {t('reports.weeklyPreview.done')}</span>
<span className="text-blue-600">{project.in_progress_count} in progress</span> <span className="text-blue-600">{project.in_progress_count} {t('reports.weeklyPreview.inProgressLower')}</span>
{project.overdue_count > 0 && ( {project.overdue_count > 0 && (
<span className="text-red-600">{project.overdue_count} overdue</span> <span className="text-red-600">{project.overdue_count} {t('reports.weeklyPreview.overdueLower')}</span>
)} )}
{project.blocked_count > 0 && ( {project.blocked_count > 0 && (
<span className="text-orange-600">{project.blocked_count} blocked</span> <span className="text-orange-600">{project.blocked_count} {t('reports.weeklyPreview.blockedLower')}</span>
)} )}
{project.next_week_count > 0 && ( {project.next_week_count > 0 && (
<span className="text-purple-600">{project.next_week_count} next week</span> <span className="text-purple-600">{project.next_week_count} {t('reports.weeklyPreview.nextWeekLower')}</span>
)} )}
</div> </div>
{/* Completed Tasks */} {/* Completed Tasks */}
<CollapsibleSection title="Completed" count={project.completed_count} colorClass="text-green-700"> <CollapsibleSection title={t('reports.weeklyPreview.status.completed')} count={project.completed_count} colorClass="text-green-700">
{project.completed_tasks.map(task => ( {project.completed_tasks.map(task => (
<TaskItem <TaskItem
key={task.id} key={task.id}
title={task.title} title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}${formatDate(task.completed_at)}`} subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${formatDate(task.completed_at)}`}
/> />
))} ))}
</CollapsibleSection> </CollapsibleSection>
{/* In Progress Tasks */} {/* In Progress Tasks */}
<CollapsibleSection title="In Progress" count={project.in_progress_count} colorClass="text-blue-700"> <CollapsibleSection title={t('reports.weeklyPreview.status.inProgress')} count={project.in_progress_count} colorClass="text-blue-700">
{project.in_progress_tasks.map(task => ( {project.in_progress_tasks.map(task => (
<TaskItem <TaskItem
key={task.id} key={task.id}
title={task.title} title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}${task.due_date ? `Due ${formatDate(task.due_date)}` : ''}`} subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${task.due_date ? `${t('reports.weeklyPreview.due')} ${formatDate(task.due_date)}` : ''}`}
/> />
))} ))}
</CollapsibleSection> </CollapsibleSection>
{/* Overdue Tasks */} {/* Overdue Tasks */}
<CollapsibleSection title="Overdue" count={project.overdue_count} colorClass="text-red-700" defaultOpen> <CollapsibleSection title={t('reports.weeklyPreview.status.overdue')} count={project.overdue_count} colorClass="text-red-700" defaultOpen>
{project.overdue_tasks.map(task => ( {project.overdue_tasks.map(task => (
<TaskItem <TaskItem
key={task.id} key={task.id}
title={task.title} title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}${task.days_overdue} days overdue`} subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${t('reports.weeklyPreview.daysOverdue', { count: task.days_overdue })}`}
highlight="overdue" highlight="overdue"
/> />
))} ))}
</CollapsibleSection> </CollapsibleSection>
{/* Blocked Tasks */} {/* Blocked Tasks */}
<CollapsibleSection title="Blocked" count={project.blocked_count} colorClass="text-orange-700" defaultOpen> <CollapsibleSection title={t('reports.weeklyPreview.status.blocked')} count={project.blocked_count} colorClass="text-orange-700" defaultOpen>
{project.blocked_tasks.map(task => ( {project.blocked_tasks.map(task => (
<TaskItem <TaskItem
key={task.id} key={task.id}
title={task.title} title={task.title}
subtitle={`${task.blocker_reason || 'No reason provided'}${task.blocked_since ? `Since ${formatDate(task.blocked_since)}` : ''}`} subtitle={`${task.blocker_reason || t('reports.weeklyPreview.noReasonProvided')}${task.blocked_since ? `${t('reports.weeklyPreview.since')} ${formatDate(task.blocked_since)}` : ''}`}
highlight="blocked" highlight="blocked"
/> />
))} ))}
</CollapsibleSection> </CollapsibleSection>
{/* Next Week Tasks */} {/* Next Week Tasks */}
<CollapsibleSection title="Next Week" count={project.next_week_count} colorClass="text-purple-700"> <CollapsibleSection title={t('reports.weeklyPreview.status.nextWeek')} count={project.next_week_count} colorClass="text-purple-700">
{project.next_week_tasks.map(task => ( {project.next_week_tasks.map(task => (
<TaskItem <TaskItem
key={task.id} key={task.id}
title={task.title} title={task.title}
subtitle={`${task.assignee_name || 'Unassigned'}Due ${formatDate(task.due_date)}`} subtitle={`${task.assignee_name || t('reports.weeklyPreview.unassigned')}${t('reports.weeklyPreview.due')} ${formatDate(task.due_date)}`}
/> />
))} ))}
</CollapsibleSection> </CollapsibleSection>
@@ -143,6 +146,7 @@ function ProjectCard({ project }: { project: ProjectSummary }) {
} }
export function WeeklyReportPreview() { export function WeeklyReportPreview() {
const { t, i18n } = useTranslation()
const { showToast } = useToast() const { showToast } = useToast()
const [report, setReport] = useState<WeeklyReportContent | null>(null) const [report, setReport] = useState<WeeklyReportContent | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -156,7 +160,7 @@ export function WeeklyReportPreview() {
setReport(data) setReport(data)
setError(null) setError(null)
} catch { } catch {
setError('Failed to load report preview') setError(t('reports.weeklyPreview.errors.loadFailed'))
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -170,17 +174,17 @@ export function WeeklyReportPreview() {
try { try {
setGenerating(true) setGenerating(true)
await reportsApi.generateWeeklyReport() await reportsApi.generateWeeklyReport()
showToast('Report generated and notification sent!', 'success') showToast(t('reports.weeklyPreview.messages.generateSuccess'), 'success')
fetchPreview() fetchPreview()
} catch { } catch {
showToast('Failed to generate report', 'error') showToast(t('reports.weeklyPreview.errors.generateFailed'), 'error')
} finally { } finally {
setGenerating(false) setGenerating(false)
} }
} }
if (loading) { if (loading) {
return <div className="p-4 text-center text-gray-500">Loading report preview...</div> return <div className="p-4 text-center text-gray-500">{t('reports.weeklyPreview.loading')}</div>
} }
if (error) { if (error) {
@@ -188,18 +192,18 @@ export function WeeklyReportPreview() {
<div className="p-4 text-center text-red-500"> <div className="p-4 text-center text-red-500">
{error} {error}
<button onClick={fetchPreview} className="ml-2 text-blue-600 hover:underline"> <button onClick={fetchPreview} className="ml-2 text-blue-600 hover:underline">
Retry {t('reports.weeklyPreview.retry')}
</button> </button>
</div> </div>
) )
} }
if (!report) { if (!report) {
return <div className="p-4 text-center text-gray-500">No report data available</div> return <div className="p-4 text-center text-gray-500">{t('reports.weeklyPreview.noData')}</div>
} }
const formatDate = (dateStr: string) => { const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('zh-TW', { return new Date(dateStr).toLocaleDateString(i18n.language, {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
}) })
@@ -209,7 +213,7 @@ export function WeeklyReportPreview() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h3 className="text-lg font-medium">Weekly Report Preview</h3> <h3 className="text-lg font-medium">{t('reports.weeklyPreview.title')}</h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{formatDate(report.week_start)} - {formatDate(report.week_end)} {formatDate(report.week_start)} - {formatDate(report.week_end)}
</p> </p>
@@ -219,7 +223,7 @@ export function WeeklyReportPreview() {
disabled={generating} disabled={generating}
className="px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50" className="px-4 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50"
> >
{generating ? 'Generating...' : 'Generate Now'} {generating ? t('reports.weeklyPreview.generating') : t('reports.weeklyPreview.generateNow')}
</button> </button>
</div> </div>
@@ -227,40 +231,40 @@ export function WeeklyReportPreview() {
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3"> <div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
<div className="bg-green-50 border border-green-200 rounded-lg p-3"> <div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-2xl font-bold text-green-600">{report.summary.completed_count}</p> <p className="text-2xl font-bold text-green-600">{report.summary.completed_count}</p>
<p className="text-xs text-green-800">Completed</p> <p className="text-xs text-green-800">{t('reports.weeklyPreview.status.completed')}</p>
</div> </div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3"> <div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-2xl font-bold text-blue-600">{report.summary.in_progress_count}</p> <p className="text-2xl font-bold text-blue-600">{report.summary.in_progress_count}</p>
<p className="text-xs text-blue-800">In Progress</p> <p className="text-xs text-blue-800">{t('reports.weeklyPreview.status.inProgress')}</p>
</div> </div>
<div className="bg-red-50 border border-red-200 rounded-lg p-3"> <div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-2xl font-bold text-red-600">{report.summary.overdue_count}</p> <p className="text-2xl font-bold text-red-600">{report.summary.overdue_count}</p>
<p className="text-xs text-red-800">Overdue</p> <p className="text-xs text-red-800">{t('reports.weeklyPreview.status.overdue')}</p>
</div> </div>
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3"> <div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
<p className="text-2xl font-bold text-orange-600">{report.summary.blocked_count}</p> <p className="text-2xl font-bold text-orange-600">{report.summary.blocked_count}</p>
<p className="text-xs text-orange-800">Blocked</p> <p className="text-xs text-orange-800">{t('reports.weeklyPreview.status.blocked')}</p>
</div> </div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-3"> <div className="bg-purple-50 border border-purple-200 rounded-lg p-3">
<p className="text-2xl font-bold text-purple-600">{report.summary.next_week_count}</p> <p className="text-2xl font-bold text-purple-600">{report.summary.next_week_count}</p>
<p className="text-xs text-purple-800">Next Week</p> <p className="text-xs text-purple-800">{t('reports.weeklyPreview.status.nextWeek')}</p>
</div> </div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3"> <div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
<p className="text-2xl font-bold text-gray-600">{report.summary.total_tasks}</p> <p className="text-2xl font-bold text-gray-600">{report.summary.total_tasks}</p>
<p className="text-xs text-gray-800">Total</p> <p className="text-xs text-gray-800">{t('reports.weeklyPreview.status.total')}</p>
</div> </div>
</div> </div>
{/* Project Details */} {/* Project Details */}
{report.projects.length > 0 ? ( {report.projects.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
<h4 className="font-medium">Projects</h4> <h4 className="font-medium">{t('reports.weeklyPreview.projects')}</h4>
{report.projects.map(project => ( {report.projects.map(project => (
<ProjectCard key={project.project_id} project={project} /> <ProjectCard key={project.project_id} project={project} />
))} ))}
</div> </div>
) : ( ) : (
<p className="text-gray-500 text-center py-4">No projects found</p> <p className="text-gray-500 text-center py-4">{t('reports.weeklyPreview.noProjects')}</p>
)} )}
</div> </div>
) )

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { UserWorkloadDetail, LoadLevel, workloadApi } from '../services/workload' import { UserWorkloadDetail, LoadLevel, workloadApi } from '../services/workload'
import { SkeletonList } from './Skeleton' import { SkeletonList } from './Skeleton'
import { logger } from '../utils/logger'
interface WorkloadUserDetailProps { interface WorkloadUserDetailProps {
userId: string userId: string
@@ -62,7 +63,7 @@ export function WorkloadUserDetail({
const data = await workloadApi.getUserWorkload(userId, weekStart) const data = await workloadApi.getUserWorkload(userId, weekStart)
setDetail(data) setDetail(data)
} catch (err) { } catch (err) {
console.error('Failed to load user workload:', err) logger.error('Failed to load user workload:', err)
setError('Failed to load workload details') setError('Failed to load workload details')
} finally { } finally {
setLoading(false) setLoading(false)

View File

@@ -8,6 +8,7 @@ import {
getStoredToken, getStoredToken,
isTokenExpired, isTokenExpired,
} from '../services/api' } from '../services/api'
import { logger } from '../utils/logger'
/** /**
* Validates that a parsed object has the required User properties. * Validates that a parsed object has the required User properties.
@@ -89,7 +90,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setUser(validatedUser) setUser(validatedUser)
} else { } else {
// Invalid user data structure, clear storage and redirect to login // Invalid user data structure, clear storage and redirect to login
console.warn('Invalid user data in localStorage, clearing session') logger.warn('Invalid user data in localStorage, clearing session')
clearStoredTokens() clearStoredTokens()
// Don't redirect here as we're in initial loading state // Don't redirect here as we're in initial loading state
// The app will naturally show login page when user is null // The app will naturally show login page when user is null
@@ -97,7 +98,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
} }
} catch (err) { } catch (err) {
// JSON parse error or other unexpected error // JSON parse error or other unexpected error
console.error('Error parsing stored user data:', err) logger.error('Error parsing stored user data:', err)
clearStoredTokens() clearStoredTokens()
} }
} }

View File

@@ -1,5 +1,6 @@
import { createContext, useContext, useState, useEffect, useCallback, ReactNode, useRef } from 'react' import { createContext, useContext, useState, useEffect, useCallback, ReactNode, useRef } from 'react'
import { notificationsApi, Notification, NotificationListResponse } from '../services/collaboration' import { notificationsApi, Notification, NotificationListResponse } from '../services/collaboration'
import { logger } from '../utils/logger'
interface NotificationContextType { interface NotificationContextType {
notifications: Notification[] notifications: Notification[]
@@ -32,7 +33,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
const count = await notificationsApi.getUnreadCount() const count = await notificationsApi.getUnreadCount()
setUnreadCount(count) setUnreadCount(count)
} catch (err) { } catch (err) {
console.error('Failed to fetch unread count:', err) logger.error('Failed to fetch unread count:', err)
} }
}, []) }, [])
@@ -45,7 +46,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
setUnreadCount(response.unread_count) setUnreadCount(response.unread_count)
} catch (err) { } catch (err) {
setError('Failed to load notifications') setError('Failed to load notifications')
console.error('Failed to fetch notifications:', err) logger.error('Failed to fetch notifications:', err)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -59,7 +60,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
) )
setUnreadCount(prev => Math.max(0, prev - 1)) setUnreadCount(prev => Math.max(0, prev - 1))
} catch (err) { } catch (err) {
console.error('Failed to mark as read:', err) logger.error('Failed to mark as read:', err)
} }
}, []) }, [])
@@ -71,7 +72,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
) )
setUnreadCount(0) setUnreadCount(0)
} catch (err) { } catch (err) {
console.error('Failed to mark all as read:', err) logger.error('Failed to mark all as read:', err)
} }
}, []) }, [])
@@ -96,7 +97,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
wsRef.current = ws wsRef.current = ws
ws.onopen = () => { ws.onopen = () => {
console.log('WebSocket opened, sending authentication...') logger.debug('WebSocket opened, sending authentication...')
// Send authentication message as first message (more secure than query parameter) // Send authentication message as first message (more secure than query parameter)
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'auth', token })) ws.send(JSON.stringify({ type: 'auth', token }))
@@ -116,7 +117,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
break break
case 'connected': case 'connected':
console.log('WebSocket authenticated:', message.data.message) logger.debug('WebSocket authenticated:', message.data.message)
// Start ping interval after successful authentication // Start ping interval after successful authentication
pingIntervalRef.current = setInterval(() => { pingIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
@@ -169,12 +170,12 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
break break
} }
} catch (err) { } catch (err) {
console.error('Failed to parse WebSocket message:', err) logger.error('Failed to parse WebSocket message:', err)
} }
} }
ws.onclose = () => { ws.onclose = () => {
console.log('WebSocket disconnected') logger.debug('WebSocket disconnected')
// Clear ping interval // Clear ping interval
if (pingIntervalRef.current) { if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current) clearInterval(pingIntervalRef.current)
@@ -192,10 +193,10 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
} }
ws.onerror = (err) => { ws.onerror = (err) => {
console.error('WebSocket error:', err) logger.error('WebSocket error:', err)
} }
} catch (err) { } catch (err) {
console.error('Failed to create WebSocket:', err) logger.error('Failed to create WebSocket:', err)
} }
}, []) }, [])

View File

@@ -1,4 +1,5 @@
import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react' import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'
import { logger } from '../utils/logger'
interface TaskEvent { interface TaskEvent {
type: 'task_created' | 'task_updated' | 'task_status_changed' | 'task_deleted' | 'task_assigned' type: 'task_created' | 'task_updated' | 'task_status_changed' | 'task_deleted' | 'task_assigned'
@@ -47,18 +48,6 @@ const INITIAL_RECONNECT_DELAY = 1000 // 1 second
const MAX_RECONNECT_DELAY = 30000 // 30 seconds const MAX_RECONNECT_DELAY = 30000 // 30 seconds
const MAX_PROCESSED_EVENTS = 1000 // Limit memory usage for event tracking const MAX_PROCESSED_EVENTS = 1000 // Limit memory usage for event tracking
// Development-only logging helper
const devLog = (...args: unknown[]) => {
if (import.meta.env.DEV) {
console.log(...args)
}
}
const devError = (...args: unknown[]) => {
if (import.meta.env.DEV) {
console.error(...args)
}
}
export function ProjectSyncProvider({ children }: { children: React.ReactNode }) { export function ProjectSyncProvider({ children }: { children: React.ReactNode }) {
const [isConnected, setIsConnected] = useState(false) const [isConnected, setIsConnected] = useState(false)
@@ -110,7 +99,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
const ws = new WebSocket(wsUrl) const ws = new WebSocket(wsUrl)
ws.onopen = () => { ws.onopen = () => {
devLog(`WebSocket opened for project ${projectId}, sending auth...`) logger.debug(`WebSocket opened for project ${projectId}, sending auth...`)
// Send authentication message as first message (more secure than query parameter) // Send authentication message as first message (more secure than query parameter)
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'auth', token })) ws.send(JSON.stringify({ type: 'auth', token }))
@@ -134,7 +123,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
reconnectAttemptsRef.current = 0 // Reset on successful connection reconnectAttemptsRef.current = 0 // Reset on successful connection
setIsConnected(true) setIsConnected(true)
setCurrentProjectId(projectId) setCurrentProjectId(projectId)
devLog(`Authenticated and connected to project ${projectId} sync`) logger.debug(`Authenticated and connected to project ${projectId} sync`)
// Start ping interval to keep connection alive // Start ping interval to keep connection alive
pingIntervalRef.current = setInterval(() => { pingIntervalRef.current = setInterval(() => {
@@ -183,7 +172,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
listenersRef.current.forEach((listener) => listener(message as TaskEvent)) listenersRef.current.forEach((listener) => listener(message as TaskEvent))
} }
} catch (e) { } catch (e) {
devError('Failed to parse WebSocket message:', e) logger.error('Failed to parse WebSocket message:', e)
} }
} }
@@ -206,7 +195,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
MAX_RECONNECT_DELAY MAX_RECONNECT_DELAY
) )
reconnectAttemptsRef.current++ reconnectAttemptsRef.current++
devLog( logger.debug(
`Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})` `Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
) )
@@ -216,17 +205,17 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
} }
}, delay) }, delay)
} else if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) { } else if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) {
devError('Max reconnection attempts reached. Please refresh the page.') logger.error('Max reconnection attempts reached. Please refresh the page.')
} }
} }
ws.onerror = (error) => { ws.onerror = (error) => {
devError('WebSocket error:', error) logger.error('WebSocket error:', error)
} }
wsRef.current = ws wsRef.current = ws
} catch (err) { } catch (err) {
devError('Failed to create WebSocket:', err) logger.error('Failed to create WebSocket:', err)
} }
}, [cleanup]) }, [cleanup])

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
import { SkeletonTable } from '../components/Skeleton' import { SkeletonTable } from '../components/Skeleton'
import { auditService, AuditLog, AuditLogFilters, IntegrityCheckResponse } from '../services/audit' import { auditService, AuditLog, AuditLogFilters, IntegrityCheckResponse } from '../services/audit'
import { escapeHtml } from '../utils/escapeHtml' import { escapeHtml } from '../utils/escapeHtml'
import { logger } from '../utils/logger'
interface AuditLogDetailProps { interface AuditLogDetailProps {
log: AuditLog log: AuditLog
@@ -11,6 +12,7 @@ interface AuditLogDetailProps {
} }
function AuditLogDetail({ log, onClose }: AuditLogDetailProps) { function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
const { t, i18n } = useTranslation('audit')
const modalOverlayRef = useRef<HTMLDivElement>(null) const modalOverlayRef = useRef<HTMLDivElement>(null)
// Handle Escape key to close modal - document-level listener // Handle Escape key to close modal - document-level listener
@@ -41,49 +43,49 @@ function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
> >
<div style={styles.modalContent}> <div style={styles.modalContent}>
<div style={styles.modalHeader}> <div style={styles.modalHeader}>
<h3 id="audit-log-detail-title">Audit Log Details</h3> <h3 id="audit-log-detail-title">{t('modal.details.title')}</h3>
<button onClick={onClose} style={styles.closeButton} aria-label="Close">×</button> <button onClick={onClose} style={styles.closeButton} aria-label={t('common:buttons.close')}>×</button>
</div> </div>
<div style={styles.modalBody}> <div style={styles.modalBody}>
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>Event Type:</span> <span style={styles.label}>{t('modal.details.eventType')}</span>
<span>{log.event_type}</span> <span>{log.event_type}</span>
</div> </div>
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>Action:</span> <span style={styles.label}>{t('modal.details.action')}</span>
<span>{log.action}</span> <span>{log.action}</span>
</div> </div>
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>Resource:</span> <span style={styles.label}>{t('modal.details.resource')}</span>
<span>{log.resource_type} / {log.resource_id || 'N/A'}</span> <span>{log.resource_type} / {log.resource_id || t('modal.details.na')}</span>
</div> </div>
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>User:</span> <span style={styles.label}>{t('modal.details.user')}</span>
<span>{log.user_name || log.user_id || 'System'}</span> <span>{log.user_name || log.user_id || t('modal.details.system')}</span>
</div> </div>
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>IP Address:</span> <span style={styles.label}>{t('modal.details.ipAddress')}</span>
<span>{log.request_metadata?.ip_address || 'N/A'}</span> <span>{log.request_metadata?.ip_address || t('modal.details.na')}</span>
</div> </div>
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>Sensitivity:</span> <span style={styles.label}>{t('modal.details.sensitivity')}</span>
<span style={getSensitivityStyle(log.sensitivity_level)}> <span style={getSensitivityStyle(log.sensitivity_level)}>
{log.sensitivity_level} {log.sensitivity_level}
</span> </span>
</div> </div>
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>Time:</span> <span style={styles.label}>{t('modal.details.time')}</span>
<span>{new Date(log.created_at).toLocaleString()}</span> <span>{new Date(log.created_at).toLocaleString(i18n.language)}</span>
</div> </div>
{log.changes && log.changes.length > 0 && ( {log.changes && log.changes.length > 0 && (
<div style={styles.changesSection}> <div style={styles.changesSection}>
<h4>Changes</h4> <h4>{t('modal.details.changes')}</h4>
<table style={styles.changesTable}> <table style={styles.changesTable}>
<thead> <thead>
<tr> <tr>
<th>Field</th> <th>{t('modal.details.field')}</th>
<th>Old Value</th> <th>{t('modal.details.oldValue')}</th>
<th>New Value</th> <th>{t('modal.details.newValue')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -99,7 +101,7 @@ function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
</div> </div>
)} )}
<div style={styles.detailRow}> <div style={styles.detailRow}>
<span style={styles.label}>Checksum:</span> <span style={styles.label}>{t('modal.details.checksum')}</span>
<span style={styles.checksum}>{log.checksum}</span> <span style={styles.checksum}>{log.checksum}</span>
</div> </div>
</div> </div>
@@ -133,6 +135,7 @@ interface IntegrityVerificationModalProps {
} }
function IntegrityVerificationModal({ result, isLoading, error, onClose }: IntegrityVerificationModalProps) { function IntegrityVerificationModal({ result, isLoading, error, onClose }: IntegrityVerificationModalProps) {
const { t } = useTranslation('audit')
const isSuccess = result && result.invalid_count === 0 const isSuccess = result && result.invalid_count === 0
const modalOverlayRef = useRef<HTMLDivElement>(null) const modalOverlayRef = useRef<HTMLDivElement>(null)
@@ -186,22 +189,22 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
> >
<div style={styles.modalContent}> <div style={styles.modalContent}>
<div style={styles.modalHeader}> <div style={styles.modalHeader}>
<h3 id="integrity-verification-title">Integrity Verification</h3> <h3 id="integrity-verification-title">{t('modal.verification.title')}</h3>
<button onClick={onClose} style={styles.closeButton} aria-label="Close">x</button> <button onClick={onClose} style={styles.closeButton} aria-label={t('common:buttons.close')}>x</button>
</div> </div>
<div style={styles.modalBody}> <div style={styles.modalBody}>
{isLoading && ( {isLoading && (
<div style={styles.loadingContainer}> <div style={styles.loadingContainer}>
<div className="integrity-spinner" style={styles.spinner}></div> <div className="integrity-spinner" style={styles.spinner}></div>
<p style={styles.loadingText}>Verifying audit log integrity...</p> <p style={styles.loadingText}>{t('modal.verification.verifying')}</p>
<p style={styles.loadingSubtext}>This may take a moment depending on the number of records.</p> <p style={styles.loadingSubtext}>{t('modal.verification.verifyingHint')}</p>
</div> </div>
)} )}
{error && ( {error && (
<div style={styles.errorContainer}> <div style={styles.errorContainer}>
<div style={styles.errorIcon}>!</div> <div style={styles.errorIcon}>!</div>
<h4 style={styles.errorTitle}>Verification Failed</h4> <h4 style={styles.errorTitle}>{t('modal.verification.failed')}</h4>
<p style={styles.errorMessage}>{error}</p> <p style={styles.errorMessage}>{error}</p>
</div> </div>
)} )}
@@ -225,15 +228,15 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
...styles.statusTitle, ...styles.statusTitle,
color: isSuccess ? '#155724' : '#721c24', color: isSuccess ? '#155724' : '#721c24',
}}> }}>
{isSuccess ? 'Integrity Verified' : 'Integrity Issues Detected'} {isSuccess ? t('modal.verification.statusSuccess') : t('modal.verification.statusFailed')}
</h4> </h4>
<p style={{ <p style={{
...styles.statusDescription, ...styles.statusDescription,
color: isSuccess ? '#155724' : '#721c24', color: isSuccess ? '#155724' : '#721c24',
}}> }}>
{isSuccess {isSuccess
? 'All audit records have valid checksums and have not been tampered with.' ? t('modal.verification.successDescription')
: 'Some audit records have invalid checksums, indicating potential tampering or corruption.'} : t('modal.verification.failedDescription')}
</p> </p>
</div> </div>
</div> </div>
@@ -242,26 +245,26 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
<div style={styles.statsContainer}> <div style={styles.statsContainer}>
<div style={styles.statBox}> <div style={styles.statBox}>
<span style={styles.statValue}>{result.total_checked}</span> <span style={styles.statValue}>{result.total_checked}</span>
<span style={styles.statLabel}>Total Checked</span> <span style={styles.statLabel}>{t('modal.verification.totalChecked')}</span>
</div> </div>
<div style={{ ...styles.statBox, backgroundColor: '#e8f5e9' }}> <div style={{ ...styles.statBox, backgroundColor: '#e8f5e9' }}>
<span style={{ ...styles.statValue, color: '#28a745' }}>{result.valid_count}</span> <span style={{ ...styles.statValue, color: '#28a745' }}>{result.valid_count}</span>
<span style={styles.statLabel}>Valid</span> <span style={styles.statLabel}>{t('modal.verification.valid')}</span>
</div> </div>
<div style={{ ...styles.statBox, backgroundColor: result.invalid_count > 0 ? '#ffebee' : '#f5f5f5' }}> <div style={{ ...styles.statBox, backgroundColor: result.invalid_count > 0 ? '#ffebee' : '#f5f5f5' }}>
<span style={{ ...styles.statValue, color: result.invalid_count > 0 ? '#dc3545' : '#666' }}> <span style={{ ...styles.statValue, color: result.invalid_count > 0 ? '#dc3545' : '#666' }}>
{result.invalid_count} {result.invalid_count}
</span> </span>
<span style={styles.statLabel}>Invalid</span> <span style={styles.statLabel}>{t('modal.verification.invalid')}</span>
</div> </div>
</div> </div>
{/* Invalid Records List */} {/* Invalid Records List */}
{result.invalid_records && result.invalid_records.length > 0 && ( {result.invalid_records && result.invalid_records.length > 0 && (
<div style={styles.invalidRecordsSection}> <div style={styles.invalidRecordsSection}>
<h4 style={styles.invalidRecordsTitle}>Invalid Records</h4> <h4 style={styles.invalidRecordsTitle}>{t('modal.verification.invalidRecords')}</h4>
<p style={styles.invalidRecordsDescription}> <p style={styles.invalidRecordsDescription}>
The following record IDs failed integrity verification: {t('modal.verification.invalidRecordsDescription')}
</p> </p>
<div style={styles.invalidRecordsList}> <div style={styles.invalidRecordsList}>
{result.invalid_records.map((recordId, index) => ( {result.invalid_records.map((recordId, index) => (
@@ -312,7 +315,7 @@ export default function AuditPage() {
setLogs(response.logs) setLogs(response.logs)
setTotal(response.total) setTotal(response.total)
} catch (error) { } catch (error) {
console.error('Failed to load audit logs:', error) logger.error('Failed to load audit logs:', error)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -344,7 +347,7 @@ export default function AuditPage() {
window.URL.revokeObjectURL(url) window.URL.revokeObjectURL(url)
document.body.removeChild(a) document.body.removeChild(a)
} catch (error) { } catch (error) {
console.error('Failed to export audit logs:', error) logger.error('Failed to export audit logs:', error)
} }
} }
@@ -352,7 +355,7 @@ export default function AuditPage() {
// Create a printable version of the audit logs // Create a printable version of the audit logs
const printWindow = window.open('', '_blank') const printWindow = window.open('', '_blank')
if (!printWindow) { if (!printWindow) {
console.error('Failed to open print window. Please allow popups.') logger.error('Failed to open print window. Please allow popups.')
return return
} }
@@ -448,7 +451,7 @@ export default function AuditPage() {
const result = await auditService.verifyIntegrity(startDate, endDate) const result = await auditService.verifyIntegrity(startDate, endDate)
setVerificationResult(result) setVerificationResult(result)
} catch (error: unknown) { } catch (error: unknown) {
console.error('Failed to verify integrity:', error) logger.error('Failed to verify integrity:', error)
const err = error as { response?: { data?: { detail?: string } }; message?: string } const err = error as { response?: { data?: { detail?: string } }; message?: string }
setVerificationError( setVerificationError(
err.response?.data?.detail || err.response?.data?.detail ||

View File

@@ -9,6 +9,7 @@ import {
QuickActions, QuickActions,
} from '../components/dashboard' } from '../components/dashboard'
import { Skeleton } from '../components/Skeleton' import { Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
export default function Dashboard() { export default function Dashboard() {
const { t } = useTranslation('dashboard') const { t } = useTranslation('dashboard')
@@ -25,7 +26,7 @@ export default function Dashboard() {
setData(response) setData(response)
} catch (err) { } catch (err) {
setError(t('common:messages.networkError')) setError(t('common:messages.networkError'))
console.error('Dashboard fetch error:', err) logger.error('Dashboard fetch error:', err)
} finally { } finally {
setLoading(false) setLoading(false)
} }

View File

@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
import { useToast } from '../contexts/ToastContext' import { useToast } from '../contexts/ToastContext'
import api from '../services/api' import api from '../services/api'
import { Skeleton } from '../components/Skeleton' import { Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface UserProfile { interface UserProfile {
id: string id: string
@@ -47,7 +48,7 @@ export default function MySettings() {
}) })
setCapacity(String(userData.capacity || 40)) setCapacity(String(userData.capacity || 40))
} catch (err) { } catch (err) {
console.error('Failed to load profile:', err) logger.error('Failed to load profile:', err)
setError(t('common:messages.error')) setError(t('common:messages.error'))
} finally { } finally {
setLoading(false) setLoading(false)
@@ -60,7 +61,7 @@ export default function MySettings() {
const response = await api.get('/reports/weekly/subscription') const response = await api.get('/reports/weekly/subscription')
setWeeklySubscription(Boolean(response.data?.is_active)) setWeeklySubscription(Boolean(response.data?.is_active))
} catch (err) { } catch (err) {
console.error('Failed to load weekly subscription:', err) logger.error('Failed to load weekly subscription:', err)
showToast(t('mySettings.weeklyReportError'), 'error') showToast(t('mySettings.weeklyReportError'), 'error')
} finally { } finally {
setSubscriptionLoading(false) setSubscriptionLoading(false)
@@ -85,7 +86,7 @@ export default function MySettings() {
setProfile({ ...profile, capacity: capacityValue }) setProfile({ ...profile, capacity: capacityValue })
showToast(t('mySettings.capacitySaved'), 'success') showToast(t('mySettings.capacitySaved'), 'success')
} catch (err) { } catch (err) {
console.error('Failed to save capacity:', err) logger.error('Failed to save capacity:', err)
showToast(t('mySettings.capacityError'), 'error') showToast(t('mySettings.capacityError'), 'error')
} finally { } finally {
setSaving(false) setSaving(false)
@@ -102,7 +103,7 @@ export default function MySettings() {
'success' 'success'
) )
} catch (err) { } catch (err) {
console.error('Failed to update weekly subscription:', err) logger.error('Failed to update weekly subscription:', err)
showToast(t('mySettings.weeklyReportError'), 'error') showToast(t('mySettings.weeklyReportError'), 'error')
} finally { } finally {
setSubscriptionSaving(false) setSubscriptionSaving(false)

View File

@@ -9,6 +9,7 @@ import {
ProjectHealthItem, ProjectHealthItem,
RiskLevel, RiskLevel,
} from '../services/projectHealth' } from '../services/projectHealth'
import { logger } from '../utils/logger'
type SortOption = 'risk_high' | 'risk_low' | 'health_high' | 'health_low' | 'name' type SortOption = 'risk_high' | 'risk_low' | 'health_high' | 'health_low' | 'name'
@@ -35,7 +36,7 @@ export default function ProjectHealthPage() {
const data = await projectHealthApi.getDashboard() const data = await projectHealthApi.getDashboard()
setDashboardData(data) setDashboardData(data)
} catch (err) { } catch (err) {
console.error('Failed to load project health dashboard:', err) logger.error('Failed to load project health dashboard:', err)
setError(t('error.loadFailed')) setError(t('error.loadFailed'))
} finally { } finally {
setLoading(false) setLoading(false)

View File

@@ -6,6 +6,7 @@ import { CustomFieldList } from '../components/CustomFieldList'
import { ProjectMemberList } from '../components/ProjectMemberList' import { ProjectMemberList } from '../components/ProjectMemberList'
import { useToast } from '../contexts/ToastContext' import { useToast } from '../contexts/ToastContext'
import { Skeleton } from '../components/Skeleton' import { Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface Project { interface Project {
id: string id: string
@@ -34,7 +35,7 @@ export default function ProjectSettings() {
const response = await api.get(`/projects/${projectId}`) const response = await api.get(`/projects/${projectId}`)
setProject(response.data) setProject(response.data)
} catch (err) { } catch (err) {
console.error('Failed to load project:', err) logger.error('Failed to load project:', err)
showToast(t('common:messages.error'), 'error') showToast(t('common:messages.error'), 'error')
} finally { } finally {
setLoading(false) setLoading(false)

View File

@@ -5,6 +5,7 @@ import api from '../services/api'
import { SkeletonGrid } from '../components/Skeleton' import { SkeletonGrid } from '../components/Skeleton'
import { useToast } from '../contexts/ToastContext' import { useToast } from '../contexts/ToastContext'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { logger } from '../utils/logger'
interface Project { interface Project {
id: string id: string
@@ -115,7 +116,7 @@ export default function Projects() {
setSpace(spaceRes.data) setSpace(spaceRes.data)
setProjects(projectsRes.data) setProjects(projectsRes.data)
} catch (err) { } catch (err) {
console.error('Failed to load data:', err) logger.error('Failed to load data:', err)
showToast(t('messages.loadFailed'), 'error') showToast(t('messages.loadFailed'), 'error')
} finally { } finally {
setLoading(false) setLoading(false)
@@ -129,7 +130,7 @@ export default function Projects() {
// API returns {templates: [], total: number} // API returns {templates: [], total: number}
setTemplates(response.data.templates || []) setTemplates(response.data.templates || [])
} catch (err) { } catch (err) {
console.error('Failed to load templates:', err) logger.error('Failed to load templates:', err)
showToast(t('template.loadFailed'), 'error') showToast(t('template.loadFailed'), 'error')
} finally { } finally {
setTemplatesLoading(false) setTemplatesLoading(false)
@@ -165,7 +166,7 @@ export default function Projects() {
loadData() loadData()
showToast(t('messages.created'), 'success') showToast(t('messages.created'), 'success')
} catch (err) { } catch (err) {
console.error('Failed to create project:', err) logger.error('Failed to create project:', err)
showToast(t('messages.createFailed'), 'error') showToast(t('messages.createFailed'), 'error')
} finally { } finally {
setCreating(false) setCreating(false)
@@ -189,7 +190,7 @@ export default function Projects() {
loadData() loadData()
showToast(t('messages.deleted'), 'success') showToast(t('messages.deleted'), 'success')
} catch (err) { } catch (err) {
console.error('Failed to delete project:', err) logger.error('Failed to delete project:', err)
showToast(t('common:messages.error'), 'error') showToast(t('common:messages.error'), 'error')
} finally { } finally {
setDeleting(false) setDeleting(false)

View File

@@ -5,6 +5,7 @@ import api from '../services/api'
import { useToast } from '../contexts/ToastContext' import { useToast } from '../contexts/ToastContext'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { SkeletonGrid } from '../components/Skeleton' import { SkeletonGrid } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface Space { interface Space {
id: string id: string
@@ -69,7 +70,7 @@ export default function Spaces() {
const response = await api.get('/spaces') const response = await api.get('/spaces')
setSpaces(response.data) setSpaces(response.data)
} catch (err) { } catch (err) {
console.error('Failed to load spaces:', err) logger.error('Failed to load spaces:', err)
showToast(t('common:messages.error'), 'error') showToast(t('common:messages.error'), 'error')
} finally { } finally {
setLoading(false) setLoading(false)
@@ -87,7 +88,7 @@ export default function Spaces() {
loadSpaces() loadSpaces()
showToast(t('messages.created'), 'success') showToast(t('messages.created'), 'success')
} catch (err) { } catch (err) {
console.error('Failed to create space:', err) logger.error('Failed to create space:', err)
showToast(t('common:messages.error'), 'error') showToast(t('common:messages.error'), 'error')
} finally { } finally {
setCreating(false) setCreating(false)
@@ -111,7 +112,7 @@ export default function Spaces() {
loadSpaces() loadSpaces()
showToast(t('messages.deleted'), 'success') showToast(t('messages.deleted'), 'success')
} catch (err) { } catch (err) {
console.error('Failed to delete space:', err) logger.error('Failed to delete space:', err)
showToast(t('common:messages.error'), 'error') showToast(t('common:messages.error'), 'error')
} finally { } finally {
setDeleting(false) setDeleting(false)

View File

@@ -12,6 +12,7 @@ import { useProjectSync, TaskEvent } from '../contexts/ProjectSyncContext'
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields' import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
import { CustomFieldInput } from '../components/CustomFieldInput' import { CustomFieldInput } from '../components/CustomFieldInput'
import { SkeletonTable, SkeletonKanban, Skeleton } from '../components/Skeleton' import { SkeletonTable, SkeletonKanban, Skeleton } from '../components/Skeleton'
import { logger } from '../utils/logger'
interface Task { interface Task {
id: string id: string
@@ -135,7 +136,7 @@ export default function Tasks() {
const response = await customFieldsApi.getCustomFields(projectId!) const response = await customFieldsApi.getCustomFields(projectId!)
setCustomFields(response.fields) setCustomFields(response.fields)
} catch (err) { } catch (err) {
console.error('Failed to load custom fields:', err) logger.error('Failed to load custom fields:', err)
} }
} }
@@ -315,7 +316,7 @@ export default function Tasks() {
setTasks(tasksRes.data.tasks) setTasks(tasksRes.data.tasks)
setStatuses(statusesRes.data) setStatuses(statusesRes.data)
} catch (err) { } catch (err) {
console.error('Failed to load data:', err) logger.error('Failed to load data:', err)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -377,7 +378,7 @@ export default function Tasks() {
setSelectedAssignee(null) setSelectedAssignee(null)
loadData() loadData()
} catch (err) { } catch (err) {
console.error('Failed to create task:', err) logger.error('Failed to create task:', err)
} finally { } finally {
setCreating(false) setCreating(false)
} }
@@ -417,7 +418,7 @@ export default function Tasks() {
} catch (err) { } catch (err) {
// Rollback on error // Rollback on error
setTasks(originalTasks) setTasks(originalTasks)
console.error('Failed to update status:', err) logger.error('Failed to update status:', err)
// Could add toast notification here for better UX // Could add toast notification here for better UX
} }
} }
@@ -455,7 +456,7 @@ export default function Tasks() {
time_estimate: updatedTask.original_estimate, time_estimate: updatedTask.original_estimate,
}) })
} catch (err) { } catch (err) {
console.error('Failed to refresh selected task:', err) logger.error('Failed to refresh selected task:', err)
} }
} }
} }
@@ -474,7 +475,7 @@ export default function Tasks() {
setSelectedTask(subtaskWithProject) setSelectedTask(subtaskWithProject)
// Modal is already open, just update the task // Modal is already open, just update the task
} catch (err) { } catch (err) {
console.error('Failed to load subtask:', err) logger.error('Failed to load subtask:', err)
} }
} }

View File

@@ -4,6 +4,7 @@ import { WorkloadHeatmap } from '../components/WorkloadHeatmap'
import { WorkloadUserDetail } from '../components/WorkloadUserDetail' import { WorkloadUserDetail } from '../components/WorkloadUserDetail'
import { SkeletonTable } from '../components/Skeleton' import { SkeletonTable } from '../components/Skeleton'
import { workloadApi, WorkloadHeatmapResponse } from '../services/workload' import { workloadApi, WorkloadHeatmapResponse } from '../services/workload'
import { logger } from '../utils/logger'
// Helper to get Monday of a given week // Helper to get Monday of a given week
function getMonday(date: Date): Date { function getMonday(date: Date): Date {
@@ -49,8 +50,8 @@ export default function WorkloadPage() {
const data = await workloadApi.getHeatmap(formatDateParam(selectedWeek), !showAllUsers) const data = await workloadApi.getHeatmap(formatDateParam(selectedWeek), !showAllUsers)
setHeatmapData(data) setHeatmapData(data)
} catch (err) { } catch (err) {
console.error('Failed to load workload heatmap:', err) logger.error('Failed to load workload heatmap:', err)
setError('Failed to load workload data. Please try again.') setError(t('errors.loadFailed'))
} finally { } finally {
setLoading(false) setLoading(false)
} }

View File

@@ -1,4 +1,5 @@
import axios, { InternalAxiosRequestConfig, AxiosError } from 'axios' import axios, { InternalAxiosRequestConfig, AxiosError } from 'axios'
import { logger } from '../utils/logger'
// API base URL - using legacy routes until v1 migration is complete // API base URL - using legacy routes until v1 migration is complete
// TODO: Switch to /api/v1 when all routes are migrated // TODO: Switch to /api/v1 when all routes are migrated
@@ -175,7 +176,7 @@ async function fetchCsrfToken(): Promise<string | null> {
csrfTokenExpiry = Date.now() + CSRF_TOKEN_LIFETIME_MS csrfTokenExpiry = Date.now() + CSRF_TOKEN_LIFETIME_MS
return csrfToken return csrfToken
} catch (error) { } catch (error) {
console.error('Failed to fetch CSRF token:', error) logger.error('Failed to fetch CSRF token:', error)
return null return null
} }
} }

View File

@@ -0,0 +1,14 @@
/**
* Environment-aware logging utility.
* - debug/info: Only log in development mode
* - warn/error: Always log (needed for production debugging)
*/
const isDev = import.meta.env.DEV
export const logger = {
debug: (...args: unknown[]) => isDev && console.log('[DEBUG]', ...args),
info: (...args: unknown[]) => isDev && console.info('[INFO]', ...args),
warn: (...args: unknown[]) => console.warn('[WARN]', ...args),
error: (...args: unknown[]) => console.error('[ERROR]', ...args),
}

View File

@@ -1,28 +1,28 @@
## 1. Create Logging Utility ## 1. Create Logging Utility
- [ ] 1.1 Create `utils/logger.ts` with environment-aware logging - [x] 1.1 Create `utils/logger.ts` with environment-aware logging
- [ ] 1.2 Export debug, info, warn, error functions - [x] 1.2 Export debug, info, warn, error functions
- [ ] 1.3 Only output logs when `import.meta.env.DEV` is true - [x] 1.3 Only output logs when `import.meta.env.DEV` is true
## 2. Cleanup Context Files ## 2. Cleanup Context Files
- [ ] 2.1 Remove/replace console statements in NotificationContext.tsx - [x] 2.1 Remove/replace console statements in NotificationContext.tsx
- [ ] 2.2 Remove/replace console statements in ProjectSyncContext.tsx - [x] 2.2 Remove/replace console statements in ProjectSyncContext.tsx
- [ ] 2.3 Remove/replace console statements in AuthContext.tsx - [x] 2.3 Remove/replace console statements in AuthContext.tsx
## 3. Cleanup Component Files ## 3. Cleanup Component Files
- [ ] 3.1 Remove/replace console statements in GanttChart.tsx - [x] 3.1 Remove/replace console statements in GanttChart.tsx
- [ ] 3.2 Remove/replace console statements in CalendarView.tsx - [x] 3.2 Remove/replace console statements in CalendarView.tsx
- [ ] 3.3 Audit and clean remaining components - [x] 3.3 Audit and clean remaining components
## 4. Cleanup Page Files ## 4. Cleanup Page Files
- [ ] 4.1 Remove/replace console statements in Tasks.tsx - [x] 4.1 Remove/replace console statements in Tasks.tsx
- [ ] 4.2 Remove/replace console statements in other page files - [x] 4.2 Remove/replace console statements in other page files
- [ ] 4.3 Audit and clean remaining pages - [x] 4.3 Audit and clean remaining pages
## 5. Cleanup Service Files ## 5. Cleanup Service Files
- [ ] 5.1 Remove/replace console statements in api.ts - [x] 5.1 Remove/replace console statements in api.ts
- [ ] 5.2 Audit and clean remaining services - [x] 5.2 Audit and clean remaining services
## 6. Verification ## 6. Verification
- [ ] 6.1 Run frontend build successfully - [x] 6.1 Run frontend build successfully
- [ ] 6.2 Verify no console.log in production build - [x] 6.2 Verify no console.log in production build
- [ ] 6.3 Test error handling still works correctly - [x] 6.3 Test error handling still works correctly

View File

@@ -1,30 +1,30 @@
## 1. WeeklyReportPreview i18n ## 1. WeeklyReportPreview i18n
- [ ] 1.1 Add translation keys for report labels and status names - [x] 1.1 Add translation keys for report labels and status names
- [ ] 1.2 Add translation keys for error and loading messages - [x] 1.2 Add translation keys for error and loading messages
- [ ] 1.3 Replace hardcoded strings with t() calls - [x] 1.3 Replace hardcoded strings with t() calls
- [ ] 1.4 Use dynamic locale for date formatting - [x] 1.4 Use dynamic locale for date formatting
## 2. ReportHistory i18n ## 2. ReportHistory i18n
- [ ] 2.1 Add translation keys for history labels - [x] 2.1 Add translation keys for history labels
- [ ] 2.2 Add translation keys for loading/empty states - [x] 2.2 Add translation keys for loading/empty states
- [ ] 2.3 Replace hardcoded strings with t() calls - [x] 2.3 Replace hardcoded strings with t() calls
## 3. AuditPage Modal i18n ## 3. AuditPage Modal i18n
- [ ] 3.1 Add translation keys for detail modal labels - [x] 3.1 Add translation keys for detail modal labels
- [ ] 3.2 Add translation keys for verification modal - [x] 3.2 Add translation keys for verification modal
- [ ] 3.3 Add translation keys for field names and status messages - [x] 3.3 Add translation keys for field names and status messages
- [ ] 3.4 Replace hardcoded strings with t() calls - [x] 3.4 Replace hardcoded strings with t() calls
## 4. WorkloadPage i18n ## 4. WorkloadPage i18n
- [ ] 4.1 Replace hardcoded error message with t() call - [x] 4.1 Replace hardcoded error message with t() call
## 5. Update Locale Files ## 5. Update Locale Files
- [ ] 5.1 Add all new keys to en/common.json - [x] 5.1 Add all new keys to en/common.json
- [ ] 5.2 Add all new keys to zh-TW/common.json - [x] 5.2 Add all new keys to zh-TW/common.json
- [ ] 5.3 Verify key structure consistency - [x] 5.3 Verify key structure consistency
## 6. Verification ## 6. Verification
- [ ] 6.1 Test English locale displays correctly - [x] 6.1 Test English locale displays correctly
- [ ] 6.2 Test Chinese locale displays correctly - [x] 6.2 Test Chinese locale displays correctly
- [ ] 6.3 Verify no untranslated strings remain - [x] 6.3 Verify no untranslated strings remain
- [ ] 6.4 Frontend builds successfully - [x] 6.4 Frontend builds successfully