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:
@@ -52,5 +52,39 @@
|
||||
"empty": {
|
||||
"title": "No Audit Records",
|
||||
"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:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"remove": "Remove",
|
||||
"view": "View",
|
||||
"download": "Download",
|
||||
"upload": "Upload"
|
||||
"upload": "Upload",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"labels": {
|
||||
"loading": "Loading...",
|
||||
@@ -135,5 +136,62 @@
|
||||
"maxFileSize": "Maximum file size: {{size}}",
|
||||
"uploading": "Uploading {{filename}} ({{current}}/{{total}})...",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,5 +68,8 @@
|
||||
"normal": "Normal: < 80%",
|
||||
"warning": "Warning: 80% - 99%",
|
||||
"overloaded": "Overloaded: ≥ 100%"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Failed to load workload data. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,39 @@
|
||||
"empty": {
|
||||
"title": "沒有稽核記錄",
|
||||
"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 未通過完整性驗證:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"remove": "移除",
|
||||
"view": "檢視",
|
||||
"download": "下載",
|
||||
"upload": "上傳"
|
||||
"upload": "上傳",
|
||||
"retry": "重試"
|
||||
},
|
||||
"labels": {
|
||||
"loading": "載入中...",
|
||||
@@ -135,5 +136,60 @@
|
||||
"maxFileSize": "檔案大小上限:{{size}}",
|
||||
"uploading": "正在上傳 {{filename}} ({{current}}/{{total}})...",
|
||||
"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": "載入報告歷史失敗"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,5 +68,8 @@
|
||||
"normal": "正常:< 80%",
|
||||
"warning": "警告:80% - 99%",
|
||||
"overloaded": "超載:≥ 100%"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "載入工作負載資料失敗,請再試一次。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AttachmentVersionHistory } from './AttachmentVersionHistory'
|
||||
import { ConfirmModal } from './ConfirmModal'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
import { SkeletonList } from './Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface AttachmentListProps {
|
||||
taskId: string
|
||||
@@ -30,7 +31,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
|
||||
const response = await attachmentService.listAttachments(taskId)
|
||||
setAttachments(response.attachments)
|
||||
} catch (error) {
|
||||
console.error('Failed to load attachments:', error)
|
||||
logger.error('Failed to load attachments:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -44,7 +45,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
|
||||
try {
|
||||
await attachmentService.downloadAttachment(attachment.id)
|
||||
} catch (error) {
|
||||
console.error('Failed to download attachment:', error)
|
||||
logger.error('Failed to download attachment:', error)
|
||||
showToast('Failed to download file', 'error')
|
||||
}
|
||||
}
|
||||
@@ -61,7 +62,7 @@ export function AttachmentList({ taskId, onRefresh }: AttachmentListProps) {
|
||||
onRefresh?.()
|
||||
showToast('File deleted successfully', 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to delete attachment:', error)
|
||||
logger.error('Failed to delete attachment:', error)
|
||||
showToast('Failed to delete file', 'error')
|
||||
} finally {
|
||||
setDeleting(null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useEffect, DragEvent, ChangeEvent } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { attachmentService } from '../services/attachments'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
// Spinner animation keyframes - injected once via useEffect
|
||||
const SPINNER_KEYFRAMES_ID = 'attachment-upload-spinner-keyframes'
|
||||
@@ -93,7 +94,7 @@ export function AttachmentUpload({ taskId, onUploadComplete }: AttachmentUploadP
|
||||
setUploadProgress(null)
|
||||
onUploadComplete?.()
|
||||
} catch (err: unknown) {
|
||||
console.error('Upload failed:', err)
|
||||
logger.error('Upload failed:', err)
|
||||
const errorMessage = err instanceof Error ? err.message : t('attachments.uploadFailed')
|
||||
setError(errorMessage)
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { attachmentService, AttachmentVersion, VersionHistoryResponse } from '../services/attachments'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface AttachmentVersionHistoryProps {
|
||||
attachmentId: string
|
||||
@@ -50,7 +51,7 @@ export function AttachmentVersionHistory({
|
||||
const response: VersionHistoryResponse = await attachmentService.getVersionHistory(attachmentId)
|
||||
setVersions(response.versions)
|
||||
} catch (err) {
|
||||
console.error('Failed to load version history:', err)
|
||||
logger.error('Failed to load version history:', err)
|
||||
setError('Failed to load version history')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -72,7 +73,7 @@ export function AttachmentVersionHistory({
|
||||
onRestore()
|
||||
onClose()
|
||||
} catch (err) {
|
||||
console.error('Failed to restore version:', err)
|
||||
logger.error('Failed to restore version:', err)
|
||||
setError('Failed to restore version. Please try again.')
|
||||
} finally {
|
||||
setRestoring(null)
|
||||
@@ -83,7 +84,7 @@ export function AttachmentVersionHistory({
|
||||
try {
|
||||
await attachmentService.downloadAttachment(attachmentId, version)
|
||||
} catch (err) {
|
||||
console.error('Failed to download version:', err)
|
||||
logger.error('Failed to download version:', err)
|
||||
setError('Failed to download version')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import interactionPlugin from '@fullcalendar/interaction'
|
||||
import { EventClickArg, EventDropArg, DatesSetArg } from '@fullcalendar/core'
|
||||
import api from '../services/api'
|
||||
import { Skeleton } from './Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
@@ -109,7 +110,7 @@ export function CalendarView({
|
||||
Array.from(uniqueAssignees.entries()).map(([id, name]) => ({ id, name }))
|
||||
)
|
||||
} 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)
|
||||
} catch (err) {
|
||||
console.error('Failed to load tasks:', err)
|
||||
logger.error('Failed to load tasks:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -277,7 +278,7 @@ export function CalendarView({
|
||||
onTaskUpdate()
|
||||
}
|
||||
}
|
||||
console.error('Failed to update task date:', err)
|
||||
logger.error('Failed to update task date:', err)
|
||||
// Rollback on error
|
||||
dropInfo.revert()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { customFieldsApi, CustomField, FieldType } from '../services/customFields'
|
||||
import { CustomFieldEditor } from './CustomFieldEditor'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface CustomFieldListProps {
|
||||
projectId: string
|
||||
@@ -39,7 +40,7 @@ export function CustomFieldList({ projectId }: CustomFieldListProps) {
|
||||
const response = await customFieldsApi.getCustomFields(projectId)
|
||||
setFields(response.fields)
|
||||
} catch (err) {
|
||||
console.error('Failed to load custom fields:', err)
|
||||
logger.error('Failed to load custom fields:', err)
|
||||
setError(t('customFields.loadError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
// Error logging service - can be extended to send to external service
|
||||
export interface ErrorLog {
|
||||
@@ -25,13 +26,12 @@ export function logError(error: Error, errorInfo: ErrorInfo): ErrorLog {
|
||||
|
||||
errorLogs.push(log)
|
||||
|
||||
// Log to console for debugging
|
||||
console.group('ErrorBoundary caught an error')
|
||||
console.error('Error:', error)
|
||||
console.error('Component Stack:', errorInfo.componentStack)
|
||||
console.error('Timestamp:', log.timestamp.toISOString())
|
||||
console.error('URL:', log.url)
|
||||
console.groupEnd()
|
||||
// Log to console for debugging (always logged as these are critical errors)
|
||||
logger.error('ErrorBoundary caught an error')
|
||||
logger.error('Error:', error)
|
||||
logger.error('Component Stack:', errorInfo.componentStack)
|
||||
logger.error('Timestamp:', log.timestamp.toISOString())
|
||||
logger.error('URL:', log.url)
|
||||
|
||||
// In production, could send to error tracking service
|
||||
// sendToErrorTrackingService(log)
|
||||
|
||||
@@ -5,6 +5,7 @@ import api from '../services/api'
|
||||
import { dependenciesApi, TaskDependency, DependencyType } from '../services/dependencies'
|
||||
import { CircularDependencyError, parseCircularError } from './CircularDependencyError'
|
||||
import { escapeHtml } from '../utils/escapeHtml'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface CycleDetails {
|
||||
cycle: string[]
|
||||
@@ -91,7 +92,7 @@ export function GanttChart({
|
||||
const deps = await dependenciesApi.getProjectDependencies(projectId)
|
||||
setDependencies(deps)
|
||||
} 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)
|
||||
onTaskUpdate()
|
||||
} 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 } } } }
|
||||
// Handle 409 Conflict - version mismatch
|
||||
if (error.response?.status === 409) {
|
||||
@@ -306,8 +307,8 @@ export function GanttChart({
|
||||
// Handle progress change
|
||||
const handleProgressChange = async (taskId: string, progress: number) => {
|
||||
// Progress changes could update task status in the future
|
||||
// For now, just log it
|
||||
console.log(`Task ${taskId} progress changed to ${progress}%`)
|
||||
// For now, just log it for debugging
|
||||
logger.debug(`Task ${taskId} progress changed to ${progress}%`)
|
||||
}
|
||||
|
||||
// Add dependency
|
||||
@@ -328,7 +329,7 @@ export function GanttChart({
|
||||
setSelectedPredecessor('')
|
||||
setSelectedDependencyType('FS')
|
||||
} 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 errorDetail = error.response?.data?.detail
|
||||
|
||||
@@ -353,7 +354,7 @@ export function GanttChart({
|
||||
await dependenciesApi.removeDependency(dependencyId)
|
||||
await loadDependencies()
|
||||
} catch (err) {
|
||||
console.error('Failed to remove dependency:', err)
|
||||
logger.error('Failed to remove dependency:', err)
|
||||
setError('Failed to remove dependency')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useToast } from '../contexts/ToastContext'
|
||||
import { Skeleton } from './Skeleton'
|
||||
import { ConfirmModal } from './ConfirmModal'
|
||||
import { AddMemberModal } from './AddMemberModal'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface ProjectMemberListProps {
|
||||
projectId: string
|
||||
@@ -28,7 +29,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
|
||||
const response = await projectMembersApi.list(projectId)
|
||||
setMembers(response.members)
|
||||
} catch (err) {
|
||||
console.error('Failed to load project members:', err)
|
||||
logger.error('Failed to load project members:', err)
|
||||
setError(t('members.loadError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -47,7 +48,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
|
||||
setIsAddModalOpen(false)
|
||||
showToast(t('members.memberAdded'), 'success')
|
||||
} 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 axiosError = err as { response?: { data?: { detail?: string } } }
|
||||
if (axiosError.response?.data?.detail) {
|
||||
@@ -70,7 +71,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
|
||||
setMemberToRemove(null)
|
||||
showToast(t('messages.memberRemoved'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to remove member:', err)
|
||||
logger.error('Failed to remove member:', err)
|
||||
showToast(t('members.removeError'), 'error')
|
||||
} finally {
|
||||
setActionLoading(false)
|
||||
@@ -94,7 +95,7 @@ export function ProjectMemberList({ projectId }: ProjectMemberListProps) {
|
||||
setEditingMemberId(null)
|
||||
showToast(t('messages.roleChanged'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to update member role:', err)
|
||||
logger.error('Failed to update member role:', err)
|
||||
showToast(t('members.roleChangeError'), 'error')
|
||||
} finally {
|
||||
setActionLoading(false)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { reportsApi, ReportHistoryItem } from '../services/reports'
|
||||
|
||||
export function ReportHistory() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [reports, setReports] = useState<ReportHistoryItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -15,7 +17,7 @@ export function ReportHistory() {
|
||||
setTotal(data.total)
|
||||
setError(null)
|
||||
} catch {
|
||||
setError('Failed to load report history')
|
||||
setError(t('reports.history.errors.loadFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -26,7 +28,7 @@ export function ReportHistory() {
|
||||
}, [])
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('zh-TW', {
|
||||
return new Date(dateStr).toLocaleString(i18n.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@@ -36,7 +38,7 @@ export function ReportHistory() {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -44,7 +46,7 @@ export function ReportHistory() {
|
||||
<div className="p-4 text-center text-red-500">
|
||||
{error}
|
||||
<button onClick={fetchHistory} className="ml-2 text-blue-600 hover:underline">
|
||||
Retry
|
||||
{t('reports.history.retry')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -53,7 +55,7 @@ export function ReportHistory() {
|
||||
if (reports.length === 0) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -61,8 +63,8 @@ export function ReportHistory() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium">Report History</h3>
|
||||
<span className="text-sm text-gray-500">{total} reports</span>
|
||||
<h3 className="text-lg font-medium">{t('reports.history.title')}</h3>
|
||||
<span className="text-sm text-gray-500">{t('reports.history.totalReports', { count: total })}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -80,9 +82,9 @@ export function ReportHistory() {
|
||||
<p className="font-medium">{formatDate(report.generated_at)}</p>
|
||||
{report.status === 'sent' && summary && (
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Completed: {summary.completed_count || 0} |
|
||||
In Progress: {summary.in_progress_count || 0} |
|
||||
Overdue: {summary.overdue_count || 0}
|
||||
{t('reports.history.completed')}: {summary.completed_count || 0} |
|
||||
{t('reports.history.inProgress')}: {summary.in_progress_count || 0} |
|
||||
{t('reports.history.overdue')}: {summary.overdue_count || 0}
|
||||
</p>
|
||||
)}
|
||||
{report.status === 'failed' && report.error_message && (
|
||||
@@ -94,7 +96,7 @@ export function ReportHistory() {
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{report.status}
|
||||
{t(`reports.history.status.${report.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { auditService, AuditLog } from '../services/audit'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface ResourceHistoryProps {
|
||||
resourceType: string
|
||||
@@ -18,7 +19,7 @@ export function ResourceHistory({ resourceType, resourceId, title = 'Change Hist
|
||||
const response = await auditService.getResourceHistory(resourceType, resourceId, 10)
|
||||
setLogs(response.logs)
|
||||
} catch (error) {
|
||||
console.error('Failed to load resource history:', error)
|
||||
logger.error('Failed to load resource history:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import api from '../services/api'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface Subtask {
|
||||
id: string
|
||||
@@ -44,7 +45,7 @@ export function SubtaskList({
|
||||
setSubtasks(response.data.tasks || [])
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch subtasks:', err)
|
||||
logger.error('Failed to fetch subtasks:', err)
|
||||
setError(t('subtasks.error.load'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -71,7 +72,7 @@ export function SubtaskList({
|
||||
fetchSubtasks()
|
||||
onSubtaskCreated?.()
|
||||
} 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 errorMessage = axiosError.response?.data?.detail || t('subtasks.error.create')
|
||||
setError(errorMessage)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { UserSearchResult } from '../services/collaboration'
|
||||
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
|
||||
import { CustomFieldInput } from './CustomFieldInput'
|
||||
import { SkeletonList } from './Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
@@ -88,7 +89,7 @@ export function TaskDetailModal({
|
||||
const response = await customFieldsApi.getCustomFields(task.project_id)
|
||||
setCustomFields(response.fields)
|
||||
} catch (err) {
|
||||
console.error('Failed to load custom fields:', err)
|
||||
logger.error('Failed to load custom fields:', err)
|
||||
} finally {
|
||||
setLoadingCustomFields(false)
|
||||
}
|
||||
@@ -222,7 +223,7 @@ export function TaskDetailModal({
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error('Failed to update task:', err)
|
||||
logger.error('Failed to update task:', err)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { usersApi, UserSearchResult } from '../services/collaboration'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface UserSelectProps {
|
||||
value: string | null
|
||||
@@ -46,7 +47,7 @@ export function UserSelect({
|
||||
const results = await usersApi.search(query)
|
||||
setUsers(results)
|
||||
} catch (err) {
|
||||
console.error('Failed to search users:', err)
|
||||
logger.error('Failed to search users:', err)
|
||||
setUsers([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { reportsApi, WeeklyReportContent, ProjectSummary } from '../services/reports'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
|
||||
@@ -53,9 +54,11 @@ function TaskItem({ title, subtitle, highlight }: TaskItemProps) {
|
||||
}
|
||||
|
||||
function ProjectCard({ project }: { project: ProjectSummary }) {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
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 (
|
||||
@@ -63,78 +66,78 @@ function ProjectCard({ project }: { project: ProjectSummary }) {
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h5 className="font-medium">{project.project_title}</h5>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Summary row */}
|
||||
<div className="flex flex-wrap gap-3 text-sm mb-2">
|
||||
<span className="text-green-600">{project.completed_count} done</span>
|
||||
<span className="text-blue-600">{project.in_progress_count} in progress</span>
|
||||
<span className="text-green-600">{project.completed_count} {t('reports.weeklyPreview.done')}</span>
|
||||
<span className="text-blue-600">{project.in_progress_count} {t('reports.weeklyPreview.inProgressLower')}</span>
|
||||
{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 && (
|
||||
<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 && (
|
||||
<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>
|
||||
|
||||
{/* 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 => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
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>
|
||||
|
||||
{/* 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 => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
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>
|
||||
|
||||
{/* 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 => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
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"
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* 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 => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
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"
|
||||
/>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* 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 => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
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>
|
||||
@@ -143,6 +146,7 @@ function ProjectCard({ project }: { project: ProjectSummary }) {
|
||||
}
|
||||
|
||||
export function WeeklyReportPreview() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { showToast } = useToast()
|
||||
const [report, setReport] = useState<WeeklyReportContent | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -156,7 +160,7 @@ export function WeeklyReportPreview() {
|
||||
setReport(data)
|
||||
setError(null)
|
||||
} catch {
|
||||
setError('Failed to load report preview')
|
||||
setError(t('reports.weeklyPreview.errors.loadFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -170,17 +174,17 @@ export function WeeklyReportPreview() {
|
||||
try {
|
||||
setGenerating(true)
|
||||
await reportsApi.generateWeeklyReport()
|
||||
showToast('Report generated and notification sent!', 'success')
|
||||
showToast(t('reports.weeklyPreview.messages.generateSuccess'), 'success')
|
||||
fetchPreview()
|
||||
} catch {
|
||||
showToast('Failed to generate report', 'error')
|
||||
showToast(t('reports.weeklyPreview.errors.generateFailed'), 'error')
|
||||
} finally {
|
||||
setGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -188,18 +192,18 @@ export function WeeklyReportPreview() {
|
||||
<div className="p-4 text-center text-red-500">
|
||||
{error}
|
||||
<button onClick={fetchPreview} className="ml-2 text-blue-600 hover:underline">
|
||||
Retry
|
||||
{t('reports.weeklyPreview.retry')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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) => {
|
||||
return new Date(dateStr).toLocaleDateString('zh-TW', {
|
||||
return new Date(dateStr).toLocaleDateString(i18n.language, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
@@ -209,7 +213,7 @@ export function WeeklyReportPreview() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<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">
|
||||
{formatDate(report.week_start)} - {formatDate(report.week_end)}
|
||||
</p>
|
||||
@@ -219,7 +223,7 @@ export function WeeklyReportPreview() {
|
||||
disabled={generating}
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -227,40 +231,40 @@ export function WeeklyReportPreview() {
|
||||
<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">
|
||||
<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 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-xs text-blue-800">In Progress</p>
|
||||
<p className="text-xs text-blue-800">{t('reports.weeklyPreview.status.inProgress')}</p>
|
||||
</div>
|
||||
<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-xs text-red-800">Overdue</p>
|
||||
<p className="text-xs text-red-800">{t('reports.weeklyPreview.status.overdue')}</p>
|
||||
</div>
|
||||
<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-xs text-orange-800">Blocked</p>
|
||||
<p className="text-xs text-orange-800">{t('reports.weeklyPreview.status.blocked')}</p>
|
||||
</div>
|
||||
<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-xs text-purple-800">Next Week</p>
|
||||
<p className="text-xs text-purple-800">{t('reports.weeklyPreview.status.nextWeek')}</p>
|
||||
</div>
|
||||
<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-xs text-gray-800">Total</p>
|
||||
<p className="text-xs text-gray-800">{t('reports.weeklyPreview.status.total')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Details */}
|
||||
{report.projects.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">Projects</h4>
|
||||
<h4 className="font-medium">{t('reports.weeklyPreview.projects')}</h4>
|
||||
{report.projects.map(project => (
|
||||
<ProjectCard key={project.project_id} project={project} />
|
||||
))}
|
||||
</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>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { UserWorkloadDetail, LoadLevel, workloadApi } from '../services/workload'
|
||||
import { SkeletonList } from './Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface WorkloadUserDetailProps {
|
||||
userId: string
|
||||
@@ -62,7 +63,7 @@ export function WorkloadUserDetail({
|
||||
const data = await workloadApi.getUserWorkload(userId, weekStart)
|
||||
setDetail(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load user workload:', err)
|
||||
logger.error('Failed to load user workload:', err)
|
||||
setError('Failed to load workload details')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getStoredToken,
|
||||
isTokenExpired,
|
||||
} from '../services/api'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
/**
|
||||
* Validates that a parsed object has the required User properties.
|
||||
@@ -89,7 +90,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setUser(validatedUser)
|
||||
} else {
|
||||
// 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()
|
||||
// Don't redirect here as we're in initial loading state
|
||||
// The app will naturally show login page when user is null
|
||||
@@ -97,7 +98,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
} catch (err) {
|
||||
// JSON parse error or other unexpected error
|
||||
console.error('Error parsing stored user data:', err)
|
||||
logger.error('Error parsing stored user data:', err)
|
||||
clearStoredTokens()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback, ReactNode, useRef } from 'react'
|
||||
import { notificationsApi, Notification, NotificationListResponse } from '../services/collaboration'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface NotificationContextType {
|
||||
notifications: Notification[]
|
||||
@@ -32,7 +33,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
const count = await notificationsApi.getUnreadCount()
|
||||
setUnreadCount(count)
|
||||
} 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)
|
||||
} catch (err) {
|
||||
setError('Failed to load notifications')
|
||||
console.error('Failed to fetch notifications:', err)
|
||||
logger.error('Failed to fetch notifications:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -59,7 +60,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
)
|
||||
setUnreadCount(prev => Math.max(0, prev - 1))
|
||||
} 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)
|
||||
} 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
|
||||
|
||||
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)
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'auth', token }))
|
||||
@@ -116,7 +117,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
break
|
||||
|
||||
case 'connected':
|
||||
console.log('WebSocket authenticated:', message.data.message)
|
||||
logger.debug('WebSocket authenticated:', message.data.message)
|
||||
// Start ping interval after successful authentication
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
@@ -169,12 +170,12 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse WebSocket message:', err)
|
||||
logger.error('Failed to parse WebSocket message:', err)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected')
|
||||
logger.debug('WebSocket disconnected')
|
||||
// Clear ping interval
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current)
|
||||
@@ -192,10 +193,10 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('WebSocket error:', err)
|
||||
logger.error('WebSocket error:', err)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create WebSocket:', err)
|
||||
logger.error('Failed to create WebSocket:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface TaskEvent {
|
||||
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_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 }) {
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
@@ -110,7 +99,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
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)
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
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
|
||||
setIsConnected(true)
|
||||
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
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
@@ -183,7 +172,7 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
|
||||
listenersRef.current.forEach((listener) => listener(message as TaskEvent))
|
||||
}
|
||||
} 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
|
||||
)
|
||||
reconnectAttemptsRef.current++
|
||||
devLog(
|
||||
logger.debug(
|
||||
`Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`
|
||||
)
|
||||
|
||||
@@ -216,17 +205,17 @@ export function ProjectSyncProvider({ children }: { children: React.ReactNode })
|
||||
}
|
||||
}, delay)
|
||||
} 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) => {
|
||||
devError('WebSocket error:', error)
|
||||
logger.error('WebSocket error:', error)
|
||||
}
|
||||
|
||||
wsRef.current = ws
|
||||
} catch (err) {
|
||||
devError('Failed to create WebSocket:', err)
|
||||
logger.error('Failed to create WebSocket:', err)
|
||||
}
|
||||
}, [cleanup])
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
|
||||
import { SkeletonTable } from '../components/Skeleton'
|
||||
import { auditService, AuditLog, AuditLogFilters, IntegrityCheckResponse } from '../services/audit'
|
||||
import { escapeHtml } from '../utils/escapeHtml'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface AuditLogDetailProps {
|
||||
log: AuditLog
|
||||
@@ -11,6 +12,7 @@ interface AuditLogDetailProps {
|
||||
}
|
||||
|
||||
function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
|
||||
const { t, i18n } = useTranslation('audit')
|
||||
const modalOverlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 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.modalHeader}>
|
||||
<h3 id="audit-log-detail-title">Audit Log Details</h3>
|
||||
<button onClick={onClose} style={styles.closeButton} aria-label="Close">×</button>
|
||||
<h3 id="audit-log-detail-title">{t('modal.details.title')}</h3>
|
||||
<button onClick={onClose} style={styles.closeButton} aria-label={t('common:buttons.close')}>×</button>
|
||||
</div>
|
||||
<div style={styles.modalBody}>
|
||||
<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>
|
||||
</div>
|
||||
<div style={styles.detailRow}>
|
||||
<span style={styles.label}>Action:</span>
|
||||
<span style={styles.label}>{t('modal.details.action')}</span>
|
||||
<span>{log.action}</span>
|
||||
</div>
|
||||
<div style={styles.detailRow}>
|
||||
<span style={styles.label}>Resource:</span>
|
||||
<span>{log.resource_type} / {log.resource_id || 'N/A'}</span>
|
||||
<span style={styles.label}>{t('modal.details.resource')}</span>
|
||||
<span>{log.resource_type} / {log.resource_id || t('modal.details.na')}</span>
|
||||
</div>
|
||||
<div style={styles.detailRow}>
|
||||
<span style={styles.label}>User:</span>
|
||||
<span>{log.user_name || log.user_id || 'System'}</span>
|
||||
<span style={styles.label}>{t('modal.details.user')}</span>
|
||||
<span>{log.user_name || log.user_id || t('modal.details.system')}</span>
|
||||
</div>
|
||||
<div style={styles.detailRow}>
|
||||
<span style={styles.label}>IP Address:</span>
|
||||
<span>{log.request_metadata?.ip_address || 'N/A'}</span>
|
||||
<span style={styles.label}>{t('modal.details.ipAddress')}</span>
|
||||
<span>{log.request_metadata?.ip_address || t('modal.details.na')}</span>
|
||||
</div>
|
||||
<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)}>
|
||||
{log.sensitivity_level}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.detailRow}>
|
||||
<span style={styles.label}>Time:</span>
|
||||
<span>{new Date(log.created_at).toLocaleString()}</span>
|
||||
<span style={styles.label}>{t('modal.details.time')}</span>
|
||||
<span>{new Date(log.created_at).toLocaleString(i18n.language)}</span>
|
||||
</div>
|
||||
{log.changes && log.changes.length > 0 && (
|
||||
<div style={styles.changesSection}>
|
||||
<h4>Changes</h4>
|
||||
<h4>{t('modal.details.changes')}</h4>
|
||||
<table style={styles.changesTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Old Value</th>
|
||||
<th>New Value</th>
|
||||
<th>{t('modal.details.field')}</th>
|
||||
<th>{t('modal.details.oldValue')}</th>
|
||||
<th>{t('modal.details.newValue')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -99,7 +101,7 @@ function AuditLogDetail({ log, onClose }: AuditLogDetailProps) {
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +135,7 @@ interface IntegrityVerificationModalProps {
|
||||
}
|
||||
|
||||
function IntegrityVerificationModal({ result, isLoading, error, onClose }: IntegrityVerificationModalProps) {
|
||||
const { t } = useTranslation('audit')
|
||||
const isSuccess = result && result.invalid_count === 0
|
||||
const modalOverlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -186,22 +189,22 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
|
||||
>
|
||||
<div style={styles.modalContent}>
|
||||
<div style={styles.modalHeader}>
|
||||
<h3 id="integrity-verification-title">Integrity Verification</h3>
|
||||
<button onClick={onClose} style={styles.closeButton} aria-label="Close">x</button>
|
||||
<h3 id="integrity-verification-title">{t('modal.verification.title')}</h3>
|
||||
<button onClick={onClose} style={styles.closeButton} aria-label={t('common:buttons.close')}>x</button>
|
||||
</div>
|
||||
<div style={styles.modalBody}>
|
||||
{isLoading && (
|
||||
<div style={styles.loadingContainer}>
|
||||
<div className="integrity-spinner" style={styles.spinner}></div>
|
||||
<p style={styles.loadingText}>Verifying audit log integrity...</p>
|
||||
<p style={styles.loadingSubtext}>This may take a moment depending on the number of records.</p>
|
||||
<p style={styles.loadingText}>{t('modal.verification.verifying')}</p>
|
||||
<p style={styles.loadingSubtext}>{t('modal.verification.verifyingHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div style={styles.errorContainer}>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@@ -225,15 +228,15 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
|
||||
...styles.statusTitle,
|
||||
color: isSuccess ? '#155724' : '#721c24',
|
||||
}}>
|
||||
{isSuccess ? 'Integrity Verified' : 'Integrity Issues Detected'}
|
||||
{isSuccess ? t('modal.verification.statusSuccess') : t('modal.verification.statusFailed')}
|
||||
</h4>
|
||||
<p style={{
|
||||
...styles.statusDescription,
|
||||
color: isSuccess ? '#155724' : '#721c24',
|
||||
}}>
|
||||
{isSuccess
|
||||
? 'All audit records have valid checksums and have not been tampered with.'
|
||||
: 'Some audit records have invalid checksums, indicating potential tampering or corruption.'}
|
||||
? t('modal.verification.successDescription')
|
||||
: t('modal.verification.failedDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,26 +245,26 @@ function IntegrityVerificationModal({ result, isLoading, error, onClose }: Integ
|
||||
<div style={styles.statsContainer}>
|
||||
<div style={styles.statBox}>
|
||||
<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 style={{ ...styles.statBox, backgroundColor: '#e8f5e9' }}>
|
||||
<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 style={{ ...styles.statBox, backgroundColor: result.invalid_count > 0 ? '#ffebee' : '#f5f5f5' }}>
|
||||
<span style={{ ...styles.statValue, color: result.invalid_count > 0 ? '#dc3545' : '#666' }}>
|
||||
{result.invalid_count}
|
||||
</span>
|
||||
<span style={styles.statLabel}>Invalid</span>
|
||||
<span style={styles.statLabel}>{t('modal.verification.invalid')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invalid Records List */}
|
||||
{result.invalid_records && result.invalid_records.length > 0 && (
|
||||
<div style={styles.invalidRecordsSection}>
|
||||
<h4 style={styles.invalidRecordsTitle}>Invalid Records</h4>
|
||||
<h4 style={styles.invalidRecordsTitle}>{t('modal.verification.invalidRecords')}</h4>
|
||||
<p style={styles.invalidRecordsDescription}>
|
||||
The following record IDs failed integrity verification:
|
||||
{t('modal.verification.invalidRecordsDescription')}
|
||||
</p>
|
||||
<div style={styles.invalidRecordsList}>
|
||||
{result.invalid_records.map((recordId, index) => (
|
||||
@@ -312,7 +315,7 @@ export default function AuditPage() {
|
||||
setLogs(response.logs)
|
||||
setTotal(response.total)
|
||||
} catch (error) {
|
||||
console.error('Failed to load audit logs:', error)
|
||||
logger.error('Failed to load audit logs:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -344,7 +347,7 @@ export default function AuditPage() {
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} 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
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) {
|
||||
console.error('Failed to open print window. Please allow popups.')
|
||||
logger.error('Failed to open print window. Please allow popups.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -448,7 +451,7 @@ export default function AuditPage() {
|
||||
const result = await auditService.verifyIntegrity(startDate, endDate)
|
||||
setVerificationResult(result)
|
||||
} 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 }
|
||||
setVerificationError(
|
||||
err.response?.data?.detail ||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
QuickActions,
|
||||
} from '../components/dashboard'
|
||||
import { Skeleton } from '../components/Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useTranslation('dashboard')
|
||||
@@ -25,7 +26,7 @@ export default function Dashboard() {
|
||||
setData(response)
|
||||
} catch (err) {
|
||||
setError(t('common:messages.networkError'))
|
||||
console.error('Dashboard fetch error:', err)
|
||||
logger.error('Dashboard fetch error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from '../contexts/AuthContext'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
import api from '../services/api'
|
||||
import { Skeleton } from '../components/Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface UserProfile {
|
||||
id: string
|
||||
@@ -47,7 +48,7 @@ export default function MySettings() {
|
||||
})
|
||||
setCapacity(String(userData.capacity || 40))
|
||||
} catch (err) {
|
||||
console.error('Failed to load profile:', err)
|
||||
logger.error('Failed to load profile:', err)
|
||||
setError(t('common:messages.error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -60,7 +61,7 @@ export default function MySettings() {
|
||||
const response = await api.get('/reports/weekly/subscription')
|
||||
setWeeklySubscription(Boolean(response.data?.is_active))
|
||||
} catch (err) {
|
||||
console.error('Failed to load weekly subscription:', err)
|
||||
logger.error('Failed to load weekly subscription:', err)
|
||||
showToast(t('mySettings.weeklyReportError'), 'error')
|
||||
} finally {
|
||||
setSubscriptionLoading(false)
|
||||
@@ -85,7 +86,7 @@ export default function MySettings() {
|
||||
setProfile({ ...profile, capacity: capacityValue })
|
||||
showToast(t('mySettings.capacitySaved'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to save capacity:', err)
|
||||
logger.error('Failed to save capacity:', err)
|
||||
showToast(t('mySettings.capacityError'), 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
@@ -102,7 +103,7 @@ export default function MySettings() {
|
||||
'success'
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Failed to update weekly subscription:', err)
|
||||
logger.error('Failed to update weekly subscription:', err)
|
||||
showToast(t('mySettings.weeklyReportError'), 'error')
|
||||
} finally {
|
||||
setSubscriptionSaving(false)
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ProjectHealthItem,
|
||||
RiskLevel,
|
||||
} from '../services/projectHealth'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
type SortOption = 'risk_high' | 'risk_low' | 'health_high' | 'health_low' | 'name'
|
||||
|
||||
@@ -35,7 +36,7 @@ export default function ProjectHealthPage() {
|
||||
const data = await projectHealthApi.getDashboard()
|
||||
setDashboardData(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load project health dashboard:', err)
|
||||
logger.error('Failed to load project health dashboard:', err)
|
||||
setError(t('error.loadFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CustomFieldList } from '../components/CustomFieldList'
|
||||
import { ProjectMemberList } from '../components/ProjectMemberList'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
import { Skeleton } from '../components/Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface Project {
|
||||
id: string
|
||||
@@ -34,7 +35,7 @@ export default function ProjectSettings() {
|
||||
const response = await api.get(`/projects/${projectId}`)
|
||||
setProject(response.data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load project:', err)
|
||||
logger.error('Failed to load project:', err)
|
||||
showToast(t('common:messages.error'), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -5,6 +5,7 @@ import api from '../services/api'
|
||||
import { SkeletonGrid } from '../components/Skeleton'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface Project {
|
||||
id: string
|
||||
@@ -115,7 +116,7 @@ export default function Projects() {
|
||||
setSpace(spaceRes.data)
|
||||
setProjects(projectsRes.data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load data:', err)
|
||||
logger.error('Failed to load data:', err)
|
||||
showToast(t('messages.loadFailed'), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -129,7 +130,7 @@ export default function Projects() {
|
||||
// API returns {templates: [], total: number}
|
||||
setTemplates(response.data.templates || [])
|
||||
} catch (err) {
|
||||
console.error('Failed to load templates:', err)
|
||||
logger.error('Failed to load templates:', err)
|
||||
showToast(t('template.loadFailed'), 'error')
|
||||
} finally {
|
||||
setTemplatesLoading(false)
|
||||
@@ -165,7 +166,7 @@ export default function Projects() {
|
||||
loadData()
|
||||
showToast(t('messages.created'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to create project:', err)
|
||||
logger.error('Failed to create project:', err)
|
||||
showToast(t('messages.createFailed'), 'error')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
@@ -189,7 +190,7 @@ export default function Projects() {
|
||||
loadData()
|
||||
showToast(t('messages.deleted'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to delete project:', err)
|
||||
logger.error('Failed to delete project:', err)
|
||||
showToast(t('common:messages.error'), 'error')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
|
||||
@@ -5,6 +5,7 @@ import api from '../services/api'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { SkeletonGrid } from '../components/Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface Space {
|
||||
id: string
|
||||
@@ -69,7 +70,7 @@ export default function Spaces() {
|
||||
const response = await api.get('/spaces')
|
||||
setSpaces(response.data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load spaces:', err)
|
||||
logger.error('Failed to load spaces:', err)
|
||||
showToast(t('common:messages.error'), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -87,7 +88,7 @@ export default function Spaces() {
|
||||
loadSpaces()
|
||||
showToast(t('messages.created'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to create space:', err)
|
||||
logger.error('Failed to create space:', err)
|
||||
showToast(t('common:messages.error'), 'error')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
@@ -111,7 +112,7 @@ export default function Spaces() {
|
||||
loadSpaces()
|
||||
showToast(t('messages.deleted'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to delete space:', err)
|
||||
logger.error('Failed to delete space:', err)
|
||||
showToast(t('common:messages.error'), 'error')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useProjectSync, TaskEvent } from '../contexts/ProjectSyncContext'
|
||||
import { customFieldsApi, CustomField, CustomValueResponse } from '../services/customFields'
|
||||
import { CustomFieldInput } from '../components/CustomFieldInput'
|
||||
import { SkeletonTable, SkeletonKanban, Skeleton } from '../components/Skeleton'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
@@ -135,7 +136,7 @@ export default function Tasks() {
|
||||
const response = await customFieldsApi.getCustomFields(projectId!)
|
||||
setCustomFields(response.fields)
|
||||
} 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)
|
||||
setStatuses(statusesRes.data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load data:', err)
|
||||
logger.error('Failed to load data:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -377,7 +378,7 @@ export default function Tasks() {
|
||||
setSelectedAssignee(null)
|
||||
loadData()
|
||||
} catch (err) {
|
||||
console.error('Failed to create task:', err)
|
||||
logger.error('Failed to create task:', err)
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
@@ -417,7 +418,7 @@ export default function Tasks() {
|
||||
} catch (err) {
|
||||
// Rollback on error
|
||||
setTasks(originalTasks)
|
||||
console.error('Failed to update status:', err)
|
||||
logger.error('Failed to update status:', err)
|
||||
// Could add toast notification here for better UX
|
||||
}
|
||||
}
|
||||
@@ -455,7 +456,7 @@ export default function Tasks() {
|
||||
time_estimate: updatedTask.original_estimate,
|
||||
})
|
||||
} 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)
|
||||
// Modal is already open, just update the task
|
||||
} catch (err) {
|
||||
console.error('Failed to load subtask:', err)
|
||||
logger.error('Failed to load subtask:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WorkloadHeatmap } from '../components/WorkloadHeatmap'
|
||||
import { WorkloadUserDetail } from '../components/WorkloadUserDetail'
|
||||
import { SkeletonTable } from '../components/Skeleton'
|
||||
import { workloadApi, WorkloadHeatmapResponse } from '../services/workload'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
// Helper to get Monday of a given week
|
||||
function getMonday(date: Date): Date {
|
||||
@@ -49,8 +50,8 @@ export default function WorkloadPage() {
|
||||
const data = await workloadApi.getHeatmap(formatDateParam(selectedWeek), !showAllUsers)
|
||||
setHeatmapData(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load workload heatmap:', err)
|
||||
setError('Failed to load workload data. Please try again.')
|
||||
logger.error('Failed to load workload heatmap:', err)
|
||||
setError(t('errors.loadFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios, { InternalAxiosRequestConfig, AxiosError } from 'axios'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
// API base URL - using legacy routes until v1 migration is complete
|
||||
// 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
|
||||
return csrfToken
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch CSRF token:', error)
|
||||
logger.error('Failed to fetch CSRF token:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
14
frontend/src/utils/logger.ts
Normal file
14
frontend/src/utils/logger.ts
Normal 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),
|
||||
}
|
||||
Reference in New Issue
Block a user