feat: complete i18n translation for remaining components
- Translate pages: Projects, ProjectHealthPage, ProjectSettings - Translate components: TaskDetailModal, KanbanBoard, Comments, SubtaskList, CalendarView, BlockerDialog - Add translation keys for tasks namespace: kanban, calendar, subtasks.error, comments.error, blockers (full translation) - Add common.labels.task translation key - Fix task creation: use original_estimate instead of time_estimate Translation coverage: - 10 locale files updated (zh-TW & en) - 6 page/component files translated - ~100 new translation keys added 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,8 @@
|
||||
"selectAssignee": "Select assignee...",
|
||||
"searchUsers": "Search users...",
|
||||
"noUsersFound": "No users found",
|
||||
"typeToSearch": "Type to search users"
|
||||
"typeToSearch": "Type to search users",
|
||||
"task": "Task"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Operation successful",
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
{
|
||||
"title": "Project Health",
|
||||
"subtitle": "Monitor the overall health of projects",
|
||||
"title": "Project Health Dashboard",
|
||||
"subtitle": "Monitor project health status and risk levels across all projects",
|
||||
"overall": {
|
||||
"title": "Overall Health",
|
||||
"healthy": "Healthy",
|
||||
"atRisk": "At Risk",
|
||||
"critical": "Critical"
|
||||
},
|
||||
"summary": {
|
||||
"totalProjects": "Total Projects",
|
||||
"healthy": "Healthy",
|
||||
"atRisk": "At Risk",
|
||||
"critical": "Critical",
|
||||
"avgHealth": "Avg. Health",
|
||||
"withBlockers": "With Blockers",
|
||||
"delayed": "Delayed"
|
||||
},
|
||||
"sort": {
|
||||
"label": "Sort by",
|
||||
"riskHigh": "Risk: High to Low",
|
||||
"riskLow": "Risk: Low to High",
|
||||
"healthHigh": "Health: High to Low",
|
||||
"healthLow": "Health: Low to High",
|
||||
"name": "Name: A to Z"
|
||||
},
|
||||
"metrics": {
|
||||
"schedule": "Schedule",
|
||||
"budget": "Budget",
|
||||
@@ -34,15 +51,21 @@
|
||||
"high": "High Risk",
|
||||
"medium": "Medium Risk",
|
||||
"low": "Low Risk",
|
||||
"critical": "Critical",
|
||||
"mitigated": "Mitigated"
|
||||
},
|
||||
"actions": {
|
||||
"viewDetails": "View Details",
|
||||
"exportReport": "Export Report",
|
||||
"setAlert": "Set Alert"
|
||||
"setAlert": "Set Alert",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"projectCount": "{{count}} project(s)",
|
||||
"empty": {
|
||||
"title": "No Health Data",
|
||||
"description": "The project needs more data to display health metrics"
|
||||
"title": "No projects found",
|
||||
"description": "Create a project to start tracking health status"
|
||||
},
|
||||
"error": {
|
||||
"loadFailed": "Failed to load project health data. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,26 @@
|
||||
"editProject": "Edit Project",
|
||||
"deleteProject": "Delete Project",
|
||||
"projectSettings": "Project Settings",
|
||||
"newProject": "New Project",
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Enter project name",
|
||||
"title": "Project Title",
|
||||
"titlePlaceholder": "Enter project title",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Enter project description",
|
||||
"descriptionPlaceholder": "Description (optional)",
|
||||
"status": "Status",
|
||||
"startDate": "Start Date",
|
||||
"endDate": "End Date",
|
||||
"owner": "Project Owner",
|
||||
"space": "Space"
|
||||
"space": "Space",
|
||||
"securityLevel": "Security Level"
|
||||
},
|
||||
"securityLevel": {
|
||||
"label": "Security Level",
|
||||
"public": "Public - All users",
|
||||
"department": "Department - Same department only",
|
||||
"confidential": "Confidential - Owner only"
|
||||
},
|
||||
"status": {
|
||||
"planning": "Planning",
|
||||
@@ -37,10 +47,18 @@
|
||||
"overdue": "Overdue",
|
||||
"progress": "Overall Progress"
|
||||
},
|
||||
"card": {
|
||||
"tasks": "{{count}} tasks",
|
||||
"owner": "Owner",
|
||||
"noDescription": "No description",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Project created",
|
||||
"created": "Project created successfully",
|
||||
"updated": "Project updated",
|
||||
"deleted": "Project deleted",
|
||||
"loadFailed": "Failed to load projects",
|
||||
"createFailed": "Failed to create project",
|
||||
"confirmDelete": "Are you sure you want to delete this project? This will delete all related tasks."
|
||||
},
|
||||
"empty": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "Settings",
|
||||
"projectSettings": "Project Settings",
|
||||
"backToTasks": "Back to Tasks",
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"members": "Members",
|
||||
@@ -13,10 +14,13 @@
|
||||
"title": "General Settings",
|
||||
"projectName": "Project Name",
|
||||
"description": "Description",
|
||||
"noDescription": "No description",
|
||||
"securityLevel": "Security Level",
|
||||
"status": "Status",
|
||||
"visibility": "Visibility",
|
||||
"public": "Public",
|
||||
"private": "Private"
|
||||
"private": "Private",
|
||||
"helpText": "To edit project details, contact the project owner."
|
||||
},
|
||||
"members": {
|
||||
"title": "Member Management",
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"attachments": "Attachments",
|
||||
"comments": "Comments",
|
||||
"watchers": "Watchers",
|
||||
"blockers": "Blockers"
|
||||
"blockers": "Blockers",
|
||||
"hours": "{{count}} hours"
|
||||
},
|
||||
"status": {
|
||||
"todo": "To Do",
|
||||
@@ -31,7 +32,11 @@
|
||||
"review": "In Review",
|
||||
"done": "Done",
|
||||
"cancelled": "Cancelled",
|
||||
"blocked": "Blocked"
|
||||
"blocked": "Blocked",
|
||||
"noStatus": "No Status",
|
||||
"unassigned": "Unassigned",
|
||||
"noDueDate": "No due date",
|
||||
"notEstimated": "Not estimated"
|
||||
},
|
||||
"priority": {
|
||||
"low": "Low",
|
||||
@@ -86,17 +91,32 @@
|
||||
"add": "Add Subtask",
|
||||
"placeholder": "Enter subtask title",
|
||||
"completed": "{{count}} / {{total}} completed",
|
||||
"empty": "No subtasks"
|
||||
"count": "{{count}} subtask(s)",
|
||||
"empty": "No subtasks",
|
||||
"adding": "Adding...",
|
||||
"error": {
|
||||
"load": "Failed to load subtasks",
|
||||
"create": "Failed to create subtask"
|
||||
}
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comments",
|
||||
"add": "Add Comment",
|
||||
"placeholder": "Write your comment...",
|
||||
"add": "Post Comment",
|
||||
"placeholder": "Add a comment... Use @name to mention someone",
|
||||
"edited": "edited",
|
||||
"delete": "Delete Comment",
|
||||
"confirmDelete": "Are you sure you want to delete this comment?",
|
||||
"confirmDelete": "Are you sure you want to delete this comment? This action cannot be undone.",
|
||||
"empty": "No comments yet",
|
||||
"reply": "Reply"
|
||||
"reply": "Reply",
|
||||
"mentioned": "Mentioned",
|
||||
"replyingTo": "Replying to comment",
|
||||
"posting": "Posting...",
|
||||
"error": {
|
||||
"load": "Failed to load comments",
|
||||
"post": "Failed to post comment",
|
||||
"update": "Failed to update comment",
|
||||
"delete": "Failed to delete comment"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
"title": "Attachments",
|
||||
@@ -113,7 +133,26 @@
|
||||
"blockedBy": "Blocked by",
|
||||
"blocking": "Blocking",
|
||||
"remove": "Remove blocker",
|
||||
"empty": "No blockers"
|
||||
"empty": "No blockers",
|
||||
"taskBlocked": "Task Blocked",
|
||||
"markAsBlocked": "Mark as Blocked",
|
||||
"activeBlocker": "Active Blocker",
|
||||
"reportedBy": "Reported by",
|
||||
"on": "on",
|
||||
"resolutionNote": "Resolution Note",
|
||||
"resolutionPlaceholder": "Describe how the blocker was resolved...",
|
||||
"resolving": "Resolving...",
|
||||
"resolveBlocker": "Resolve Blocker",
|
||||
"blockerReason": "Blocker Reason",
|
||||
"reasonPlaceholder": "Describe what is blocking this task...",
|
||||
"history": "Blocker History",
|
||||
"reported": "Reported",
|
||||
"resolved": "Resolved",
|
||||
"error": {
|
||||
"load": "Failed to load blockers",
|
||||
"create": "Failed to create blocker",
|
||||
"resolve": "Failed to resolve blocker"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"created": "Task created",
|
||||
@@ -126,6 +165,18 @@
|
||||
"attachmentUploaded": "Attachment uploaded",
|
||||
"confirmDelete": "Are you sure you want to delete this task? This action cannot be undone."
|
||||
},
|
||||
"kanban": {
|
||||
"dropHere": "Drop tasks here"
|
||||
},
|
||||
"calendar": {
|
||||
"allAssignees": "All Assignees",
|
||||
"allPriorities": "All Priorities",
|
||||
"activeOnly": "Active Only",
|
||||
"completedOnly": "Completed Only",
|
||||
"clearFilters": "Clear Filters",
|
||||
"overdue": "Overdue",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No Tasks",
|
||||
"description": "There are no tasks yet. Create your first task to get started!",
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
"selectAssignee": "選擇負責人...",
|
||||
"searchUsers": "搜尋使用者...",
|
||||
"noUsersFound": "找不到使用者",
|
||||
"typeToSearch": "輸入以搜尋使用者"
|
||||
"typeToSearch": "輸入以搜尋使用者",
|
||||
"task": "任務"
|
||||
},
|
||||
"messages": {
|
||||
"success": "操作成功",
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
{
|
||||
"title": "專案健康度",
|
||||
"subtitle": "監控專案的整體健康狀況",
|
||||
"title": "專案健康度儀表板",
|
||||
"subtitle": "監控所有專案的健康狀況與風險等級",
|
||||
"overall": {
|
||||
"title": "整體健康度",
|
||||
"healthy": "健康",
|
||||
"atRisk": "風險中",
|
||||
"critical": "危急"
|
||||
},
|
||||
"summary": {
|
||||
"totalProjects": "專案總數",
|
||||
"healthy": "健康",
|
||||
"atRisk": "風險中",
|
||||
"critical": "危急",
|
||||
"avgHealth": "平均健康度",
|
||||
"withBlockers": "有阻擋問題",
|
||||
"delayed": "延遲"
|
||||
},
|
||||
"sort": {
|
||||
"label": "排序方式",
|
||||
"riskHigh": "風險:高到低",
|
||||
"riskLow": "風險:低到高",
|
||||
"healthHigh": "健康度:高到低",
|
||||
"healthLow": "健康度:低到高",
|
||||
"name": "名稱:A 到 Z"
|
||||
},
|
||||
"metrics": {
|
||||
"schedule": "進度",
|
||||
"budget": "預算",
|
||||
@@ -34,15 +51,21 @@
|
||||
"high": "高風險",
|
||||
"medium": "中風險",
|
||||
"low": "低風險",
|
||||
"critical": "危急",
|
||||
"mitigated": "已緩解"
|
||||
},
|
||||
"actions": {
|
||||
"viewDetails": "查看詳情",
|
||||
"exportReport": "匯出報告",
|
||||
"setAlert": "設定警示"
|
||||
"setAlert": "設定警示",
|
||||
"retry": "重試"
|
||||
},
|
||||
"projectCount": "{{count}} 個專案",
|
||||
"empty": {
|
||||
"title": "沒有健康度資料",
|
||||
"description": "專案需要更多資料才能顯示健康度指標"
|
||||
"title": "沒有專案",
|
||||
"description": "建立專案以開始追蹤健康狀態"
|
||||
},
|
||||
"error": {
|
||||
"loadFailed": "載入專案健康度資料失敗,請重試。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,26 @@
|
||||
"editProject": "編輯專案",
|
||||
"deleteProject": "刪除專案",
|
||||
"projectSettings": "專案設定",
|
||||
"newProject": "新增專案",
|
||||
"fields": {
|
||||
"name": "名稱",
|
||||
"namePlaceholder": "輸入專案名稱",
|
||||
"title": "專案標題",
|
||||
"titlePlaceholder": "輸入專案標題",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "輸入專案描述",
|
||||
"descriptionPlaceholder": "輸入專案描述(選填)",
|
||||
"status": "狀態",
|
||||
"startDate": "開始日期",
|
||||
"endDate": "結束日期",
|
||||
"owner": "專案負責人",
|
||||
"space": "工作空間"
|
||||
"space": "工作空間",
|
||||
"securityLevel": "安全等級"
|
||||
},
|
||||
"securityLevel": {
|
||||
"label": "安全等級",
|
||||
"public": "公開 - 所有使用者",
|
||||
"department": "部門 - 僅同部門",
|
||||
"confidential": "機密 - 僅負責人"
|
||||
},
|
||||
"status": {
|
||||
"planning": "規劃中",
|
||||
@@ -37,10 +47,18 @@
|
||||
"overdue": "逾期",
|
||||
"progress": "整體進度"
|
||||
},
|
||||
"card": {
|
||||
"tasks": "{{count}} 個任務",
|
||||
"owner": "負責人",
|
||||
"noDescription": "沒有描述",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"messages": {
|
||||
"created": "專案已建立",
|
||||
"updated": "專案已更新",
|
||||
"deleted": "專案已刪除",
|
||||
"loadFailed": "載入專案失敗",
|
||||
"createFailed": "建立專案失敗",
|
||||
"confirmDelete": "確定要刪除此專案嗎?此操作將刪除所有相關任務。"
|
||||
},
|
||||
"empty": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "設定",
|
||||
"projectSettings": "專案設定",
|
||||
"backToTasks": "返回任務",
|
||||
"tabs": {
|
||||
"general": "一般",
|
||||
"members": "成員",
|
||||
@@ -13,10 +14,13 @@
|
||||
"title": "一般設定",
|
||||
"projectName": "專案名稱",
|
||||
"description": "描述",
|
||||
"noDescription": "沒有描述",
|
||||
"securityLevel": "安全等級",
|
||||
"status": "狀態",
|
||||
"visibility": "可見性",
|
||||
"public": "公開",
|
||||
"private": "私人"
|
||||
"private": "私人",
|
||||
"helpText": "如需編輯專案詳情,請聯繫專案負責人。"
|
||||
},
|
||||
"members": {
|
||||
"title": "成員管理",
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"attachments": "附件",
|
||||
"comments": "留言",
|
||||
"watchers": "關注者",
|
||||
"blockers": "阻擋項目"
|
||||
"blockers": "阻擋項目",
|
||||
"hours": "{{count}} 小時"
|
||||
},
|
||||
"status": {
|
||||
"todo": "待處理",
|
||||
@@ -31,7 +32,11 @@
|
||||
"review": "審核中",
|
||||
"done": "已完成",
|
||||
"cancelled": "已取消",
|
||||
"blocked": "被阻擋"
|
||||
"blocked": "被阻擋",
|
||||
"noStatus": "無狀態",
|
||||
"unassigned": "未指派",
|
||||
"noDueDate": "無截止日期",
|
||||
"notEstimated": "未預估"
|
||||
},
|
||||
"priority": {
|
||||
"low": "低",
|
||||
@@ -86,17 +91,32 @@
|
||||
"add": "新增子任務",
|
||||
"placeholder": "輸入子任務標題",
|
||||
"completed": "已完成 {{count}} / {{total}}",
|
||||
"empty": "沒有子任務"
|
||||
"count": "{{count}} 個子任務",
|
||||
"empty": "沒有子任務",
|
||||
"adding": "新增中...",
|
||||
"error": {
|
||||
"load": "載入子任務失敗",
|
||||
"create": "建立子任務失敗"
|
||||
}
|
||||
},
|
||||
"comments": {
|
||||
"title": "留言",
|
||||
"add": "新增留言",
|
||||
"placeholder": "輸入您的留言...",
|
||||
"add": "發表留言",
|
||||
"placeholder": "新增留言... 使用 @name 來提及某人",
|
||||
"edited": "已編輯",
|
||||
"delete": "刪除留言",
|
||||
"confirmDelete": "確定要刪除此留言嗎?",
|
||||
"confirmDelete": "確定要刪除此留言嗎?此操作無法復原。",
|
||||
"empty": "還沒有留言",
|
||||
"reply": "回覆"
|
||||
"reply": "回覆",
|
||||
"mentioned": "提及",
|
||||
"replyingTo": "回覆留言中",
|
||||
"posting": "發送中...",
|
||||
"error": {
|
||||
"load": "載入留言失敗",
|
||||
"post": "發表留言失敗",
|
||||
"update": "更新留言失敗",
|
||||
"delete": "刪除留言失敗"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
"title": "附件",
|
||||
@@ -113,7 +133,26 @@
|
||||
"blockedBy": "被以下任務阻擋",
|
||||
"blocking": "正在阻擋以下任務",
|
||||
"remove": "移除阻擋關係",
|
||||
"empty": "沒有阻擋項目"
|
||||
"empty": "沒有阻擋項目",
|
||||
"taskBlocked": "任務被阻擋",
|
||||
"markAsBlocked": "標記為阻擋",
|
||||
"activeBlocker": "進行中的阻擋",
|
||||
"reportedBy": "回報者",
|
||||
"on": "於",
|
||||
"resolutionNote": "解決說明",
|
||||
"resolutionPlaceholder": "描述如何解決阻擋問題...",
|
||||
"resolving": "解決中...",
|
||||
"resolveBlocker": "解決阻擋",
|
||||
"blockerReason": "阻擋原因",
|
||||
"reasonPlaceholder": "描述阻擋此任務的原因...",
|
||||
"history": "阻擋歷史",
|
||||
"reported": "回報",
|
||||
"resolved": "已解決",
|
||||
"error": {
|
||||
"load": "載入阻擋項目失敗",
|
||||
"create": "建立阻擋項目失敗",
|
||||
"resolve": "解決阻擋項目失敗"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"created": "任務已建立",
|
||||
@@ -126,6 +165,18 @@
|
||||
"attachmentUploaded": "附件已上傳",
|
||||
"confirmDelete": "確定要刪除此任務嗎?此操作無法復原。"
|
||||
},
|
||||
"kanban": {
|
||||
"dropHere": "將任務拖放至此"
|
||||
},
|
||||
"calendar": {
|
||||
"allAssignees": "所有負責人",
|
||||
"allPriorities": "所有優先順序",
|
||||
"activeOnly": "僅進行中",
|
||||
"completedOnly": "僅已完成",
|
||||
"clearFilters": "清除篩選",
|
||||
"overdue": "逾期",
|
||||
"completed": "已完成"
|
||||
},
|
||||
"empty": {
|
||||
"title": "沒有任務",
|
||||
"description": "目前沒有任務。建立您的第一個任務開始吧!",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { blockersApi, Blocker } from '../services/collaboration'
|
||||
import { SkeletonList } from './Skeleton'
|
||||
|
||||
@@ -17,6 +18,7 @@ export function BlockerDialog({
|
||||
onClose,
|
||||
onBlockerChange,
|
||||
}: BlockerDialogProps) {
|
||||
const { t } = useTranslation('tasks')
|
||||
const [blockers, setBlockers] = useState<Blocker[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [reason, setReason] = useState('')
|
||||
@@ -34,7 +36,7 @@ export function BlockerDialog({
|
||||
const active = response.blockers.find(b => !b.resolved_at)
|
||||
setActiveBlocker(active || null)
|
||||
} catch (err) {
|
||||
setError('Failed to load blockers')
|
||||
setError(t('blockers.error.load'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -55,7 +57,7 @@ export function BlockerDialog({
|
||||
setError(null)
|
||||
onBlockerChange()
|
||||
} catch (err) {
|
||||
setError('Failed to create blocker')
|
||||
setError(t('blockers.error.create'))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -74,7 +76,7 @@ export function BlockerDialog({
|
||||
setError(null)
|
||||
onBlockerChange()
|
||||
} catch (err) {
|
||||
setError('Failed to resolve blocker')
|
||||
setError(t('blockers.error.resolve'))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -85,7 +87,7 @@ export function BlockerDialog({
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden">
|
||||
<div className="p-4 border-b flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{isBlocked ? 'Task Blocked' : 'Mark as Blocked'}
|
||||
{isBlocked ? t('blockers.taskBlocked') : t('blockers.markAsBlocked')}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -96,7 +98,7 @@ export function BlockerDialog({
|
||||
</div>
|
||||
|
||||
<div className="p-4 overflow-y-auto max-h-[60vh]">
|
||||
<p className="text-gray-600 mb-4">Task: {taskTitle}</p>
|
||||
<p className="text-gray-600 mb-4">{t('common:labels.task')}: {taskTitle}</p>
|
||||
|
||||
{error && (
|
||||
<div className="p-2 bg-red-100 text-red-700 rounded text-sm mb-4">
|
||||
@@ -114,23 +116,23 @@ export function BlockerDialog({
|
||||
{activeBlocker && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<h3 className="font-semibold text-red-800 mb-2">
|
||||
Active Blocker
|
||||
{t('blockers.activeBlocker')}
|
||||
</h3>
|
||||
<p className="text-gray-700 mb-2">{activeBlocker.reason}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Reported by {activeBlocker.reporter.name} on{' '}
|
||||
{t('blockers.reportedBy')} {activeBlocker.reporter.name} {t('blockers.on')}{' '}
|
||||
{new Date(activeBlocker.created_at).toLocaleString()}
|
||||
</p>
|
||||
|
||||
{/* Resolve form */}
|
||||
<form onSubmit={handleResolveBlocker} className="mt-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Resolution Note
|
||||
{t('blockers.resolutionNote')}
|
||||
</label>
|
||||
<textarea
|
||||
value={resolutionNote}
|
||||
onChange={e => setResolutionNote(e.target.value)}
|
||||
placeholder="Describe how the blocker was resolved..."
|
||||
placeholder={t('blockers.resolutionPlaceholder')}
|
||||
className="w-full p-2 border rounded resize-none"
|
||||
rows={3}
|
||||
required
|
||||
@@ -140,7 +142,7 @@ export function BlockerDialog({
|
||||
disabled={submitting || !resolutionNote.trim()}
|
||||
className="mt-2 px-4 py-2 bg-green-600 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Resolving...' : 'Resolve Blocker'}
|
||||
{submitting ? t('blockers.resolving') : t('blockers.resolveBlocker')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -150,12 +152,12 @@ export function BlockerDialog({
|
||||
{!activeBlocker && (
|
||||
<form onSubmit={handleCreateBlocker} className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Blocker Reason
|
||||
{t('blockers.blockerReason')}
|
||||
</label>
|
||||
<textarea
|
||||
value={reason}
|
||||
onChange={e => setReason(e.target.value)}
|
||||
placeholder="Describe what is blocking this task..."
|
||||
placeholder={t('blockers.reasonPlaceholder')}
|
||||
className="w-full p-2 border rounded resize-none"
|
||||
rows={3}
|
||||
required
|
||||
@@ -165,7 +167,7 @@ export function BlockerDialog({
|
||||
disabled={submitting || !reason.trim()}
|
||||
className="mt-2 px-4 py-2 bg-red-600 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Creating...' : 'Mark as Blocked'}
|
||||
{submitting ? t('common:labels.loading') : t('blockers.markAsBlocked')}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
@@ -173,7 +175,7 @@ export function BlockerDialog({
|
||||
{/* History */}
|
||||
{blockers.filter(b => b.resolved_at).length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h3 className="font-semibold mb-2">Blocker History</h3>
|
||||
<h3 className="font-semibold mb-2">{t('blockers.history')}</h3>
|
||||
<div className="space-y-2">
|
||||
{blockers
|
||||
.filter(b => b.resolved_at)
|
||||
@@ -184,12 +186,12 @@ export function BlockerDialog({
|
||||
>
|
||||
<p className="font-medium">{blocker.reason}</p>
|
||||
<p className="text-gray-500 mt-1">
|
||||
Reported: {blocker.reporter.name} •{' '}
|
||||
{t('blockers.reported')}: {blocker.reporter.name} •{' '}
|
||||
{new Date(blocker.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
{blocker.resolver && (
|
||||
<p className="text-green-600 mt-1">
|
||||
Resolved: {blocker.resolver.name} •{' '}
|
||||
{t('blockers.resolved')}: {blocker.resolver.name} •{' '}
|
||||
{new Date(blocker.resolved_at!).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
@@ -212,7 +214,7 @@ export function BlockerDialog({
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
||||
>
|
||||
Close
|
||||
{t('common:buttons.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FullCalendar from '@fullcalendar/react'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
@@ -75,6 +76,7 @@ export function CalendarView({
|
||||
onTaskClick,
|
||||
onTaskUpdate,
|
||||
}: CalendarViewProps) {
|
||||
const { t } = useTranslation('tasks')
|
||||
const calendarRef = useRef<FullCalendar>(null)
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -306,13 +308,13 @@ export function CalendarView({
|
||||
{/* Filter Controls */}
|
||||
<div style={styles.filterBar}>
|
||||
<div style={styles.filterGroup}>
|
||||
<label style={styles.filterLabel}>Assignee</label>
|
||||
<label style={styles.filterLabel}>{t('fields.assignee')}</label>
|
||||
<select
|
||||
value={filterAssignee}
|
||||
onChange={(e) => setFilterAssignee(e.target.value)}
|
||||
style={styles.filterSelect}
|
||||
>
|
||||
<option value="">All Assignees</option>
|
||||
<option value="">{t('calendar.allAssignees')}</option>
|
||||
{assignees.map((assignee) => (
|
||||
<option key={assignee.id} value={assignee.id}>
|
||||
{assignee.name}
|
||||
@@ -322,36 +324,36 @@ export function CalendarView({
|
||||
</div>
|
||||
|
||||
<div style={styles.filterGroup}>
|
||||
<label style={styles.filterLabel}>Status</label>
|
||||
<label style={styles.filterLabel}>{t('fields.status')}</label>
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
style={styles.filterSelect}
|
||||
>
|
||||
<option value="all">All Tasks</option>
|
||||
<option value="active">Active Only</option>
|
||||
<option value="completed">Completed Only</option>
|
||||
<option value="all">{t('filters.all')}</option>
|
||||
<option value="active">{t('calendar.activeOnly')}</option>
|
||||
<option value="completed">{t('calendar.completedOnly')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={styles.filterGroup}>
|
||||
<label style={styles.filterLabel}>Priority</label>
|
||||
<label style={styles.filterLabel}>{t('fields.priority')}</label>
|
||||
<select
|
||||
value={filterPriority}
|
||||
onChange={(e) => setFilterPriority(e.target.value)}
|
||||
style={styles.filterSelect}
|
||||
>
|
||||
<option value="">All Priorities</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="">{t('calendar.allPriorities')}</option>
|
||||
<option value="urgent">{t('priority.urgent')}</option>
|
||||
<option value="high">{t('priority.high')}</option>
|
||||
<option value="medium">{t('priority.medium')}</option>
|
||||
<option value="low">{t('priority.low')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<button onClick={handleClearFilters} style={styles.clearFiltersButton}>
|
||||
Clear Filters
|
||||
{t('calendar.clearFilters')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -392,17 +394,17 @@ export function CalendarView({
|
||||
<div style={styles.legend}>
|
||||
<div style={styles.legendItem}>
|
||||
<span style={{ ...styles.legendDot, backgroundColor: '#ffebee', border: '2px solid #f44336' }} />
|
||||
<span style={styles.legendText}>Overdue</span>
|
||||
<span style={styles.legendText}>{t('calendar.overdue')}</span>
|
||||
</div>
|
||||
<div style={styles.legendItem}>
|
||||
<span style={{ ...styles.legendDot, backgroundColor: '#e8f5e9', border: '2px solid #4caf50' }} />
|
||||
<span style={styles.legendText}>Completed</span>
|
||||
<span style={styles.legendText}>{t('calendar.completed')}</span>
|
||||
</div>
|
||||
<div style={styles.legendItem}>
|
||||
<span style={styles.legendText}>Priority:</span>
|
||||
<span style={{ ...styles.priorityLabel, color: priorityColors.urgent }}>!!! Urgent</span>
|
||||
<span style={{ ...styles.priorityLabel, color: priorityColors.high }}>!! High</span>
|
||||
<span style={{ ...styles.priorityLabel, color: priorityColors.medium }}>! Medium</span>
|
||||
<span style={styles.legendText}>{t('fields.priority')}:</span>
|
||||
<span style={{ ...styles.priorityLabel, color: priorityColors.urgent }}>!!! {t('priority.urgent')}</span>
|
||||
<span style={{ ...styles.priorityLabel, color: priorityColors.high }}>!! {t('priority.high')}</span>
|
||||
<span style={{ ...styles.priorityLabel, color: priorityColors.medium }}>! {t('priority.medium')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { commentsApi, usersApi, Comment, UserSearchResult } from '../services/collaboration'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { ConfirmModal } from './ConfirmModal'
|
||||
@@ -9,6 +10,7 @@ interface CommentsProps {
|
||||
}
|
||||
|
||||
export function Comments({ taskId }: CommentsProps) {
|
||||
const { t } = useTranslation('tasks')
|
||||
const { user } = useAuth()
|
||||
const [comments, setComments] = useState<Comment[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -29,7 +31,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
setComments(response.comments)
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError('Failed to load comments')
|
||||
setError(t('comments.error.load'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -55,7 +57,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
setNewComment('')
|
||||
setReplyTo(null)
|
||||
} catch (err) {
|
||||
setError('Failed to post comment')
|
||||
setError(t('comments.error.post'))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -69,7 +71,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
setEditingId(null)
|
||||
setEditContent('')
|
||||
} catch (err) {
|
||||
setError('Failed to update comment')
|
||||
setError(t('comments.error.update'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +81,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
await fetchComments()
|
||||
setDeleteConfirm(null)
|
||||
} catch (err) {
|
||||
setError('Failed to delete comment')
|
||||
setError(t('comments.error.delete'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-lg">Comments ({comments.length})</h3>
|
||||
<h3 className="font-semibold text-lg">{t('comments.title')} ({comments.length})</h3>
|
||||
|
||||
{error && (
|
||||
<div className="p-2 bg-red-100 text-red-700 rounded text-sm">{error}</div>
|
||||
@@ -136,7 +138,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
{new Date(comment.created_at).toLocaleString()}
|
||||
</span>
|
||||
{comment.is_edited && (
|
||||
<span className="text-gray-400 text-xs ml-1">(edited)</span>
|
||||
<span className="text-gray-400 text-xs ml-1">({t('comments.edited')})</span>
|
||||
)}
|
||||
</div>
|
||||
{user?.id === comment.author.id && !comment.is_deleted && (
|
||||
@@ -148,13 +150,13 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
Edit
|
||||
{t('common:buttons.edit')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(comment.id)}
|
||||
className="text-sm text-red-600 hover:underline"
|
||||
>
|
||||
Delete
|
||||
{t('common:buttons.delete')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -173,13 +175,13 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
onClick={() => handleEdit(comment.id)}
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm"
|
||||
>
|
||||
Save
|
||||
{t('common:buttons.save')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
className="px-3 py-1 bg-gray-300 rounded text-sm"
|
||||
>
|
||||
Cancel
|
||||
{t('common:buttons.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,7 +194,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
{/* Mentions */}
|
||||
{comment.mentions.length > 0 && (
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
Mentioned: {comment.mentions.map(m => m.name).join(', ')}
|
||||
{t('comments.mentioned')}: {comment.mentions.map(m => m.name).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -202,7 +204,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
onClick={() => setReplyTo(comment.id)}
|
||||
className="text-sm text-blue-600 hover:underline mt-2"
|
||||
>
|
||||
Reply {comment.reply_count > 0 && `(${comment.reply_count})`}
|
||||
{t('comments.reply')} {comment.reply_count > 0 && `(${comment.reply_count})`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -213,13 +215,13 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
<form onSubmit={handleSubmit} className="space-y-2">
|
||||
{replyTo && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<span>Replying to comment</span>
|
||||
<span>{t('comments.replyingTo')}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReplyTo(null)}
|
||||
className="text-red-600 hover:underline"
|
||||
>
|
||||
Cancel
|
||||
{t('common:buttons.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -227,7 +229,7 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
<textarea
|
||||
value={newComment}
|
||||
onChange={e => handleInputChange(e.target.value)}
|
||||
placeholder="Add a comment... Use @name to mention someone"
|
||||
placeholder={t('comments.placeholder')}
|
||||
className="w-full p-3 border rounded-lg resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
@@ -253,16 +255,16 @@ export function Comments({ taskId }: CommentsProps) {
|
||||
disabled={!newComment.trim() || submitting}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Posting...' : 'Post Comment'}
|
||||
{submitting ? t('comments.posting') : t('comments.add')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={deleteConfirm !== null}
|
||||
title="Delete Comment"
|
||||
message="Are you sure you want to delete this comment? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
title={t('comments.delete')}
|
||||
message={t('comments.confirmDelete')}
|
||||
confirmText={t('common:buttons.delete')}
|
||||
cancelText={t('common:buttons.cancel')}
|
||||
confirmStyle="danger"
|
||||
onConfirm={() => deleteConfirm && handleDelete(deleteConfirm)}
|
||||
onCancel={() => setDeleteConfirm(null)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CustomValueResponse } from '../services/customFields'
|
||||
|
||||
interface Task {
|
||||
@@ -39,6 +40,7 @@ export function KanbanBoard({
|
||||
onStatusChange,
|
||||
onTaskClick,
|
||||
}: KanbanBoardProps) {
|
||||
const { t } = useTranslation('tasks')
|
||||
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null)
|
||||
const [dragOverColumnId, setDragOverColumnId] = useState<string | null>(null)
|
||||
|
||||
@@ -135,7 +137,7 @@ export function KanbanBoard({
|
||||
</span>
|
||||
)}
|
||||
{task.subtask_count > 0 && (
|
||||
<span style={styles.subtaskBadge}>{task.subtask_count} subtasks</span>
|
||||
<span style={styles.subtaskBadge}>{t('subtasks.count', { count: task.subtask_count })}</span>
|
||||
)}
|
||||
{/* Display custom field values (limit to first 2 for compact display) */}
|
||||
{task.custom_values?.slice(0, 2).map((cv) => (
|
||||
@@ -158,7 +160,7 @@ export function KanbanBoard({
|
||||
backgroundColor: '#9e9e9e',
|
||||
}}
|
||||
>
|
||||
<span style={styles.columnTitle}>No Status</span>
|
||||
<span style={styles.columnTitle}>{t('status.noStatus')}</span>
|
||||
<span style={styles.taskCount}>{unassignedTasks.length}</span>
|
||||
</div>
|
||||
<div style={styles.taskList}>
|
||||
@@ -194,7 +196,7 @@ export function KanbanBoard({
|
||||
{tasksByStatus[status.id]?.map(renderTaskCard)}
|
||||
{(!tasksByStatus[status.id] || tasksByStatus[status.id].length === 0) && (
|
||||
<div style={styles.emptyColumn}>
|
||||
Drop tasks here
|
||||
{t('kanban.dropHere')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import api from '../services/api'
|
||||
|
||||
interface Subtask {
|
||||
@@ -25,6 +26,7 @@ export function SubtaskList({
|
||||
onSubtaskClick,
|
||||
onSubtaskCreated,
|
||||
}: SubtaskListProps) {
|
||||
const { t } = useTranslation('tasks')
|
||||
const [subtasks, setSubtasks] = useState<Subtask[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -41,7 +43,7 @@ export function SubtaskList({
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch subtasks:', err)
|
||||
setError('Failed to load subtasks')
|
||||
setError(t('subtasks.error.load'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -69,7 +71,7 @@ export function SubtaskList({
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to create subtask:', err)
|
||||
const axiosError = err as { response?: { data?: { detail?: string } } }
|
||||
const errorMessage = axiosError.response?.data?.detail || 'Failed to create subtask'
|
||||
const errorMessage = axiosError.response?.data?.detail || t('subtasks.error.create')
|
||||
setError(errorMessage)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
@@ -99,7 +101,7 @@ export function SubtaskList({
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={expanded}
|
||||
aria-label={`Subtasks section, ${subtasks.length} items`}
|
||||
aria-label={`${t('subtasks.title')}, ${subtasks.length}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
@@ -107,14 +109,14 @@ export function SubtaskList({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={styles.title}>Subtasks ({subtasks.length})</span>
|
||||
<span style={styles.title}>{t('subtasks.title')} ({subtasks.length})</span>
|
||||
<span style={styles.toggleIcon}>{expanded ? '\u25BC' : '\u25B6'}</span>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div style={styles.content}>
|
||||
{loading ? (
|
||||
<div style={styles.loadingText}>Loading subtasks...</div>
|
||||
<div style={styles.loadingText}>{t('common:labels.loading')}</div>
|
||||
) : error ? (
|
||||
<div style={styles.errorText}>{error}</div>
|
||||
) : (
|
||||
@@ -165,21 +167,21 @@ export function SubtaskList({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={styles.emptyText}>No subtasks yet</div>
|
||||
<div style={styles.emptyText}>{t('subtasks.empty')}</div>
|
||||
)}
|
||||
|
||||
{/* Add Subtask Form */}
|
||||
{showAddForm ? (
|
||||
<form onSubmit={handleAddSubtask} style={styles.addForm}>
|
||||
<label htmlFor="new-subtask-title" style={styles.visuallyHidden}>
|
||||
Subtask title
|
||||
{t('subtasks.placeholder')}
|
||||
</label>
|
||||
<input
|
||||
id="new-subtask-title"
|
||||
type="text"
|
||||
value={newSubtaskTitle}
|
||||
onChange={(e) => setNewSubtaskTitle(e.target.value)}
|
||||
placeholder="Enter subtask title..."
|
||||
placeholder={t('subtasks.placeholder')}
|
||||
style={styles.input}
|
||||
autoFocus
|
||||
disabled={submitting}
|
||||
@@ -191,14 +193,14 @@ export function SubtaskList({
|
||||
style={styles.cancelButton}
|
||||
disabled={submitting}
|
||||
>
|
||||
Cancel
|
||||
{t('common:buttons.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={styles.submitButton}
|
||||
disabled={!newSubtaskTitle.trim() || submitting}
|
||||
>
|
||||
{submitting ? 'Adding...' : 'Add'}
|
||||
{submitting ? t('subtasks.adding') : t('common:buttons.add')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -207,7 +209,7 @@ export function SubtaskList({
|
||||
onClick={() => setShowAddForm(true)}
|
||||
style={styles.addButton}
|
||||
>
|
||||
+ Add Subtask
|
||||
+ {t('subtasks.add')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import api from '../services/api'
|
||||
import { Comments } from './Comments'
|
||||
import { TaskAttachments } from './TaskAttachments'
|
||||
@@ -50,6 +51,7 @@ export function TaskDetailModal({
|
||||
onUpdate,
|
||||
onSubtaskClick,
|
||||
}: TaskDetailModalProps) {
|
||||
const { t } = useTranslation('tasks')
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [editForm, setEditForm] = useState({
|
||||
@@ -263,7 +265,7 @@ export function TaskDetailModal({
|
||||
<div style={styles.headerActions}>
|
||||
{!isEditing ? (
|
||||
<button onClick={() => setIsEditing(true)} style={styles.editButton}>
|
||||
Edit
|
||||
{t('common:buttons.edit')}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
@@ -272,18 +274,18 @@ export function TaskDetailModal({
|
||||
style={styles.cancelButton}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
{t('common:buttons.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
style={styles.saveButton}
|
||||
disabled={saving || !editForm.title.trim()}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
{saving ? t('common:labels.loading') : t('common:buttons.save')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={onClose} style={styles.closeButton} aria-label="Close">
|
||||
<button onClick={onClose} style={styles.closeButton} aria-label={t('common:buttons.close')}>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
@@ -293,7 +295,7 @@ export function TaskDetailModal({
|
||||
<div style={styles.mainSection}>
|
||||
{/* Description */}
|
||||
<div style={styles.field}>
|
||||
<label style={styles.fieldLabel}>Description</label>
|
||||
<label style={styles.fieldLabel}>{t('fields.description')}</label>
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
value={editForm.description}
|
||||
@@ -301,11 +303,11 @@ export function TaskDetailModal({
|
||||
setEditForm({ ...editForm, description: e.target.value })
|
||||
}
|
||||
style={styles.textarea}
|
||||
placeholder="Add a description..."
|
||||
placeholder={t('fields.descriptionPlaceholder')}
|
||||
/>
|
||||
) : (
|
||||
<div style={styles.descriptionText}>
|
||||
{task.description || 'No description'}
|
||||
{task.description || t('common:labels.noData')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -334,7 +336,7 @@ export function TaskDetailModal({
|
||||
<div style={styles.sidebar}>
|
||||
{/* Status */}
|
||||
<div style={styles.sidebarField}>
|
||||
<label style={styles.sidebarLabel}>Status</label>
|
||||
<label style={styles.sidebarLabel}>{t('fields.status')}</label>
|
||||
{isEditing ? (
|
||||
<select
|
||||
value={editForm.status_id}
|
||||
@@ -343,7 +345,7 @@ export function TaskDetailModal({
|
||||
}
|
||||
style={styles.select}
|
||||
>
|
||||
<option value="">No Status</option>
|
||||
<option value="">{t('status.noStatus')}</option>
|
||||
{statuses.map((status) => (
|
||||
<option key={status.id} value={status.id}>
|
||||
{status.name}
|
||||
@@ -357,14 +359,14 @@ export function TaskDetailModal({
|
||||
backgroundColor: task.status_color || '#e0e0e0',
|
||||
}}
|
||||
>
|
||||
{task.status_name || 'No Status'}
|
||||
{task.status_name || t('status.noStatus')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div style={styles.sidebarField}>
|
||||
<label style={styles.sidebarLabel}>Priority</label>
|
||||
<label style={styles.sidebarLabel}>{t('fields.priority')}</label>
|
||||
{isEditing ? (
|
||||
<select
|
||||
value={editForm.priority}
|
||||
@@ -373,10 +375,10 @@ export function TaskDetailModal({
|
||||
}
|
||||
style={styles.select}
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
<option value="low">{t('priority.low')}</option>
|
||||
<option value="medium">{t('priority.medium')}</option>
|
||||
<option value="high">{t('priority.high')}</option>
|
||||
<option value="urgent">{t('priority.urgent')}</option>
|
||||
</select>
|
||||
) : (
|
||||
<div
|
||||
@@ -386,30 +388,30 @@ export function TaskDetailModal({
|
||||
color: getPriorityColor(task.priority),
|
||||
}}
|
||||
>
|
||||
{task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}
|
||||
{t(`priority.${task.priority}`)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Assignee */}
|
||||
<div style={styles.sidebarField}>
|
||||
<label style={styles.sidebarLabel}>Assignee</label>
|
||||
<label style={styles.sidebarLabel}>{t('fields.assignee')}</label>
|
||||
{isEditing ? (
|
||||
<UserSelect
|
||||
value={editForm.assignee_id}
|
||||
onChange={handleAssigneeChange}
|
||||
placeholder="Select assignee..."
|
||||
placeholder={t('common:labels.selectAssignee')}
|
||||
/>
|
||||
) : (
|
||||
<div style={styles.assigneeDisplay}>
|
||||
{task.assignee_name || 'Unassigned'}
|
||||
{task.assignee_name || t('status.unassigned')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Due Date */}
|
||||
<div style={styles.sidebarField}>
|
||||
<label style={styles.sidebarLabel}>Due Date</label>
|
||||
<label style={styles.sidebarLabel}>{t('fields.dueDate')}</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="date"
|
||||
@@ -423,14 +425,14 @@ export function TaskDetailModal({
|
||||
<div style={styles.dueDateDisplay}>
|
||||
{task.due_date
|
||||
? new Date(task.due_date).toLocaleDateString()
|
||||
: 'No due date'}
|
||||
: t('status.noDueDate')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Time Estimate */}
|
||||
<div style={styles.sidebarField}>
|
||||
<label style={styles.sidebarLabel}>Time Estimate (hours)</label>
|
||||
<label style={styles.sidebarLabel}>{t('fields.estimatedHours')}</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
@@ -445,7 +447,7 @@ export function TaskDetailModal({
|
||||
/>
|
||||
) : (
|
||||
<div style={styles.timeEstimateDisplay}>
|
||||
{task.time_estimate ? `${task.time_estimate} hours` : 'Not estimated'}
|
||||
{task.time_estimate ? t('fields.hours', { count: task.time_estimate }) : t('status.notEstimated')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -453,8 +455,8 @@ export function TaskDetailModal({
|
||||
{/* Subtasks Info */}
|
||||
{task.subtask_count > 0 && (
|
||||
<div style={styles.sidebarField}>
|
||||
<label style={styles.sidebarLabel}>Subtasks</label>
|
||||
<div style={styles.subtaskInfo}>{task.subtask_count} subtask(s)</div>
|
||||
<label style={styles.sidebarLabel}>{t('subtasks.title')}</label>
|
||||
<div style={styles.subtaskInfo}>{t('subtasks.count', { count: task.subtask_count })}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -462,7 +464,7 @@ export function TaskDetailModal({
|
||||
{customFields.length > 0 && (
|
||||
<>
|
||||
<div style={styles.customFieldsDivider} />
|
||||
<div style={styles.customFieldsHeader}>Custom Fields</div>
|
||||
<div style={styles.customFieldsHeader}>{t('settings:customFields.title')}</div>
|
||||
{loadingCustomFields ? (
|
||||
<SkeletonList count={3} showAvatar={false} />
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ProjectHealthCard } from '../components/ProjectHealthCard'
|
||||
import { SkeletonGrid } from '../components/Skeleton'
|
||||
import {
|
||||
@@ -11,14 +12,6 @@ import {
|
||||
|
||||
type SortOption = 'risk_high' | 'risk_low' | 'health_high' | 'health_low' | 'name'
|
||||
|
||||
const sortOptions: { value: SortOption; label: string }[] = [
|
||||
{ value: 'risk_high', label: 'Risk: High to Low' },
|
||||
{ value: 'risk_low', label: 'Risk: Low to High' },
|
||||
{ value: 'health_high', label: 'Health: High to Low' },
|
||||
{ value: 'health_low', label: 'Health: Low to High' },
|
||||
{ value: 'name', label: 'Name: A to Z' },
|
||||
]
|
||||
|
||||
// Risk level priority for sorting (higher number = higher risk)
|
||||
const riskLevelPriority: Record<RiskLevel, number> = {
|
||||
low: 1,
|
||||
@@ -28,6 +21,7 @@ const riskLevelPriority: Record<RiskLevel, number> = {
|
||||
}
|
||||
|
||||
export default function ProjectHealthPage() {
|
||||
const { t } = useTranslation('health')
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -42,7 +36,7 @@ export default function ProjectHealthPage() {
|
||||
setDashboardData(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load project health dashboard:', err)
|
||||
setError('Failed to load project health data. Please try again.')
|
||||
setError(t('error.loadFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -99,9 +93,9 @@ export default function ProjectHealthPage() {
|
||||
{/* Header */}
|
||||
<div style={styles.header}>
|
||||
<div>
|
||||
<h1 style={styles.title}>Project Health Dashboard</h1>
|
||||
<h1 style={styles.title}>{t('title')}</h1>
|
||||
<p style={styles.subtitle}>
|
||||
Monitor project health status and risk levels across all projects
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,25 +105,25 @@ export default function ProjectHealthPage() {
|
||||
<div style={styles.summaryContainer}>
|
||||
<div style={styles.summaryCard}>
|
||||
<span style={styles.summaryValue}>{dashboardData.summary.total_projects}</span>
|
||||
<span style={styles.summaryLabel}>Total Projects</span>
|
||||
<span style={styles.summaryLabel}>{t('summary.totalProjects')}</span>
|
||||
</div>
|
||||
<div style={styles.summaryCard}>
|
||||
<span style={{ ...styles.summaryValue, color: '#4caf50' }}>
|
||||
{dashboardData.summary.healthy_count}
|
||||
</span>
|
||||
<span style={styles.summaryLabel}>Healthy</span>
|
||||
<span style={styles.summaryLabel}>{t('summary.healthy')}</span>
|
||||
</div>
|
||||
<div style={styles.summaryCard}>
|
||||
<span style={{ ...styles.summaryValue, color: '#ff9800' }}>
|
||||
{dashboardData.summary.at_risk_count}
|
||||
</span>
|
||||
<span style={styles.summaryLabel}>At Risk</span>
|
||||
<span style={styles.summaryLabel}>{t('summary.atRisk')}</span>
|
||||
</div>
|
||||
<div style={styles.summaryCard}>
|
||||
<span style={{ ...styles.summaryValue, color: '#f44336' }}>
|
||||
{dashboardData.summary.critical_count}
|
||||
</span>
|
||||
<span style={styles.summaryLabel}>Critical</span>
|
||||
<span style={styles.summaryLabel}>{t('summary.critical')}</span>
|
||||
</div>
|
||||
<div style={styles.summaryCard}>
|
||||
<span
|
||||
@@ -140,19 +134,19 @@ export default function ProjectHealthPage() {
|
||||
>
|
||||
{Math.round(dashboardData.summary.average_health_score)}
|
||||
</span>
|
||||
<span style={styles.summaryLabel}>Avg. Health</span>
|
||||
<span style={styles.summaryLabel}>{t('summary.avgHealth')}</span>
|
||||
</div>
|
||||
<div style={styles.summaryCard}>
|
||||
<span style={{ ...styles.summaryValue, color: dashboardData.summary.projects_with_blockers > 0 ? '#f44336' : '#666' }}>
|
||||
{dashboardData.summary.projects_with_blockers}
|
||||
</span>
|
||||
<span style={styles.summaryLabel}>With Blockers</span>
|
||||
<span style={styles.summaryLabel}>{t('summary.withBlockers')}</span>
|
||||
</div>
|
||||
<div style={styles.summaryCard}>
|
||||
<span style={{ ...styles.summaryValue, color: dashboardData.summary.projects_delayed > 0 ? '#ff9800' : '#666' }}>
|
||||
{dashboardData.summary.projects_delayed}
|
||||
</span>
|
||||
<span style={styles.summaryLabel}>Delayed</span>
|
||||
<span style={styles.summaryLabel}>{t('summary.delayed')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -162,7 +156,7 @@ export default function ProjectHealthPage() {
|
||||
<div style={styles.controlsContainer}>
|
||||
<div style={styles.sortControl}>
|
||||
<label htmlFor="sort-select" style={styles.sortLabel}>
|
||||
Sort by:
|
||||
{t('sort.label')}:
|
||||
</label>
|
||||
<select
|
||||
id="sort-select"
|
||||
@@ -170,15 +164,15 @@ export default function ProjectHealthPage() {
|
||||
onChange={handleSortChange}
|
||||
style={styles.sortSelect}
|
||||
>
|
||||
{sortOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
<option value="risk_high">{t('sort.riskHigh')}</option>
|
||||
<option value="risk_low">{t('sort.riskLow')}</option>
|
||||
<option value="health_high">{t('sort.healthHigh')}</option>
|
||||
<option value="health_low">{t('sort.healthLow')}</option>
|
||||
<option value="name">{t('sort.name')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<span style={styles.projectCount}>
|
||||
{dashboardData.projects.length} project{dashboardData.projects.length !== 1 ? 's' : ''}
|
||||
{t('projectCount', { count: dashboardData.projects.length })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -190,14 +184,14 @@ export default function ProjectHealthPage() {
|
||||
<div style={styles.errorContainer}>
|
||||
<p style={styles.error}>{error}</p>
|
||||
<button onClick={loadDashboard} style={styles.retryButton}>
|
||||
Retry
|
||||
{t('actions.retry')}
|
||||
</button>
|
||||
</div>
|
||||
) : sortedProjects.length === 0 ? (
|
||||
<div style={styles.emptyContainer}>
|
||||
<p style={styles.emptyText}>No projects found.</p>
|
||||
<p style={styles.emptyText}>{t('empty.title')}</p>
|
||||
<p style={styles.emptySubtext}>
|
||||
Create a project to start tracking health status.
|
||||
{t('empty.description')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import api from '../services/api'
|
||||
import { CustomFieldList } from '../components/CustomFieldList'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
@@ -15,6 +16,7 @@ interface Project {
|
||||
}
|
||||
|
||||
export default function ProjectSettings() {
|
||||
const { t } = useTranslation('settings')
|
||||
const { projectId } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
@@ -32,7 +34,7 @@ export default function ProjectSettings() {
|
||||
setProject(response.data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load project:', err)
|
||||
showToast('Failed to load project settings', 'error')
|
||||
showToast(t('common:messages.error'), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -59,21 +61,21 @@ export default function ProjectSettings() {
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
return <div style={styles.error}>Project not found</div>
|
||||
return <div style={styles.error}>{t('common:messages.notFound')}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.breadcrumb}>
|
||||
<span onClick={() => navigate('/spaces')} style={styles.breadcrumbLink}>
|
||||
Spaces
|
||||
{t('common:nav.spaces')}
|
||||
</span>
|
||||
<span style={styles.breadcrumbSeparator}>/</span>
|
||||
<span
|
||||
onClick={() => navigate(`/spaces/${project.space_id}`)}
|
||||
style={styles.breadcrumbLink}
|
||||
>
|
||||
Projects
|
||||
{t('common:nav.projects')}
|
||||
</span>
|
||||
<span style={styles.breadcrumbSeparator}>/</span>
|
||||
<span
|
||||
@@ -83,16 +85,16 @@ export default function ProjectSettings() {
|
||||
{project.title}
|
||||
</span>
|
||||
<span style={styles.breadcrumbSeparator}>/</span>
|
||||
<span>Settings</span>
|
||||
<span>{t('title')}</span>
|
||||
</div>
|
||||
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>Project Settings</h1>
|
||||
<h1 style={styles.title}>{t('projectSettings')}</h1>
|
||||
<button
|
||||
onClick={() => navigate(`/projects/${project.id}`)}
|
||||
style={styles.backButton}
|
||||
>
|
||||
Back to Tasks
|
||||
{t('backToTasks')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +109,7 @@ export default function ProjectSettings() {
|
||||
...(activeTab === 'general' ? styles.navItemActive : {}),
|
||||
}}
|
||||
>
|
||||
General
|
||||
{t('tabs.general')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('custom-fields')}
|
||||
@@ -116,7 +118,7 @@ export default function ProjectSettings() {
|
||||
...(activeTab === 'custom-fields' ? styles.navItemActive : {}),
|
||||
}}
|
||||
>
|
||||
Custom Fields
|
||||
{t('tabs.customFields')}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -125,25 +127,25 @@ export default function ProjectSettings() {
|
||||
<div style={styles.content}>
|
||||
{activeTab === 'general' && (
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>General Settings</h2>
|
||||
<h2 style={styles.sectionTitle}>{t('general.title')}</h2>
|
||||
<div style={styles.infoCard}>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.infoLabel}>Project Name</span>
|
||||
<span style={styles.infoLabel}>{t('general.projectName')}</span>
|
||||
<span style={styles.infoValue}>{project.title}</span>
|
||||
</div>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.infoLabel}>Description</span>
|
||||
<span style={styles.infoLabel}>{t('general.description')}</span>
|
||||
<span style={styles.infoValue}>
|
||||
{project.description || 'No description'}
|
||||
{project.description || t('general.noDescription')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.infoLabel}>Security Level</span>
|
||||
<span style={styles.infoLabel}>{t('general.securityLevel')}</span>
|
||||
<span style={styles.infoValue}>{project.security_level}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style={styles.helpText}>
|
||||
To edit project details, contact the project owner.
|
||||
{t('general.helpText')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import api from '../services/api'
|
||||
import { SkeletonGrid } from '../components/Skeleton'
|
||||
import { useToast } from '../contexts/ToastContext'
|
||||
@@ -23,6 +24,7 @@ interface Space {
|
||||
}
|
||||
|
||||
export default function Projects() {
|
||||
const { t } = useTranslation('projects')
|
||||
const { spaceId } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
@@ -71,7 +73,7 @@ export default function Projects() {
|
||||
setProjects(projectsRes.data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load data:', err)
|
||||
showToast('Failed to load projects', 'error')
|
||||
showToast(t('messages.loadFailed'), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -86,10 +88,10 @@ export default function Projects() {
|
||||
setShowCreateModal(false)
|
||||
setNewProject({ title: '', description: '', security_level: 'department' })
|
||||
loadData()
|
||||
showToast('Project created successfully', 'success')
|
||||
showToast(t('messages.created'), 'success')
|
||||
} catch (err) {
|
||||
console.error('Failed to create project:', err)
|
||||
showToast('Failed to create project', 'error')
|
||||
showToast(t('messages.createFailed'), 'error')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
@@ -125,16 +127,16 @@ export default function Projects() {
|
||||
<div style={styles.container}>
|
||||
<div style={styles.breadcrumb}>
|
||||
<span onClick={() => navigate('/spaces')} style={styles.breadcrumbLink}>
|
||||
Spaces
|
||||
{t('common:nav.spaces')}
|
||||
</span>
|
||||
<span style={styles.breadcrumbSeparator}>/</span>
|
||||
<span>{space?.name}</span>
|
||||
</div>
|
||||
|
||||
<div style={styles.header}>
|
||||
<h1 style={styles.title}>Projects</h1>
|
||||
<h1 style={styles.title}>{t('title')}</h1>
|
||||
<button onClick={() => setShowCreateModal(true)} style={styles.createButton}>
|
||||
+ New Project
|
||||
+ {t('newProject')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +154,7 @@ export default function Projects() {
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Open project: ${project.title}`}
|
||||
aria-label={`${t('title')}: ${project.title}`}
|
||||
>
|
||||
<div style={styles.cardHeader}>
|
||||
<h3 style={styles.cardTitle}>{project.title}</h3>
|
||||
@@ -161,18 +163,18 @@ export default function Projects() {
|
||||
</span>
|
||||
</div>
|
||||
<p style={styles.cardDescription}>
|
||||
{project.description || 'No description'}
|
||||
{project.description || t('card.noDescription')}
|
||||
</p>
|
||||
<div style={styles.cardMeta}>
|
||||
<span>{project.task_count} tasks</span>
|
||||
<span>Owner: {project.owner_name || 'Unknown'}</span>
|
||||
<span>{t('card.tasks', { count: project.task_count })}</span>
|
||||
<span>{t('card.owner')}: {project.owner_name || t('card.unknown')}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{projects.length === 0 && (
|
||||
<div style={styles.empty}>
|
||||
<p>No projects yet. Create your first project!</p>
|
||||
<p>{t('empty.description')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -187,49 +189,49 @@ export default function Projects() {
|
||||
aria-labelledby="create-project-title"
|
||||
>
|
||||
<div style={styles.modal}>
|
||||
<h2 id="create-project-title" style={styles.modalTitle}>Create New Project</h2>
|
||||
<h2 id="create-project-title" style={styles.modalTitle}>{t('createProject')}</h2>
|
||||
<label htmlFor="project-title" style={styles.visuallyHidden}>
|
||||
Project title
|
||||
{t('fields.title')}
|
||||
</label>
|
||||
<input
|
||||
id="project-title"
|
||||
type="text"
|
||||
placeholder="Project title"
|
||||
placeholder={t('fields.titlePlaceholder')}
|
||||
value={newProject.title}
|
||||
onChange={(e) => setNewProject({ ...newProject, title: e.target.value })}
|
||||
style={styles.input}
|
||||
autoFocus
|
||||
/>
|
||||
<label htmlFor="project-description" style={styles.visuallyHidden}>
|
||||
Description
|
||||
{t('fields.description')}
|
||||
</label>
|
||||
<textarea
|
||||
id="project-description"
|
||||
placeholder="Description (optional)"
|
||||
placeholder={t('fields.descriptionPlaceholder')}
|
||||
value={newProject.description}
|
||||
onChange={(e) => setNewProject({ ...newProject, description: e.target.value })}
|
||||
style={styles.textarea}
|
||||
/>
|
||||
<label style={styles.label}>Security Level</label>
|
||||
<label style={styles.label}>{t('securityLevel.label')}</label>
|
||||
<select
|
||||
value={newProject.security_level}
|
||||
onChange={(e) => setNewProject({ ...newProject, security_level: e.target.value })}
|
||||
style={styles.select}
|
||||
>
|
||||
<option value="public">Public - All users</option>
|
||||
<option value="department">Department - Same department only</option>
|
||||
<option value="confidential">Confidential - Owner only</option>
|
||||
<option value="public">{t('securityLevel.public')}</option>
|
||||
<option value="department">{t('securityLevel.department')}</option>
|
||||
<option value="confidential">{t('securityLevel.confidential')}</option>
|
||||
</select>
|
||||
<div style={styles.modalActions}>
|
||||
<button onClick={() => setShowCreateModal(false)} style={styles.cancelButton}>
|
||||
Cancel
|
||||
{t('common:buttons.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateProject}
|
||||
disabled={creating || !newProject.title.trim()}
|
||||
style={styles.submitButton}
|
||||
>
|
||||
{creating ? 'Creating...' : 'Create'}
|
||||
{creating ? t('common:labels.loading') : t('common:buttons.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -322,7 +322,7 @@ export default function Tasks() {
|
||||
payload.due_date = newTask.due_date
|
||||
}
|
||||
if (newTask.time_estimate) {
|
||||
payload.time_estimate = Number(newTask.time_estimate)
|
||||
payload.original_estimate = Number(newTask.time_estimate)
|
||||
}
|
||||
|
||||
// Include custom field values (only non-formula fields)
|
||||
|
||||
Reference in New Issue
Block a user