feat: implement soft delete, task editing fixes, and UI improvements

Backend:
- Add soft delete for spaces and projects (is_active flag)
- Add status_id and assignee_id to TaskUpdate schema
- Fix task PATCH endpoint to update status and assignee
- Add validation for assignee_id and status_id in task updates
- Fix health service to count tasks with "Blocked" status as blockers
- Filter out deleted spaces/projects from health dashboard
- Add workload cache invalidation on assignee changes

Frontend:
- Add delete confirmation dialogs for spaces and projects
- Fix UserSelect to display selected user name (valueName prop)
- Fix task detail modal to refresh data after save
- Enforce 2-level subtask depth limit in UI
- Fix timezone bug in date formatting (use local timezone)
- Convert NotificationBell from Tailwind to inline styles
- Add i18n translations for health, workload, settings pages
- Add parent_task_id to Task interface across components

OpenSpec:
- Archive add-delete-capability change

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-10 01:32:13 +08:00
parent 2796cbb42d
commit 55f85d0d3c
44 changed files with 1854 additions and 297 deletions

View File

@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next'
import { ProjectHealthItem, RiskLevel, ScheduleStatus, ResourceStatus } from '../services/projectHealth'
interface ProjectHealthCardProps {
@@ -13,31 +14,32 @@ function getHealthScoreColor(score: number): string {
return '#f44336' // Red
}
// Risk level colors and labels
const riskLevelConfig: Record<RiskLevel, { color: string; bgColor: string; label: string }> = {
low: { color: '#2e7d32', bgColor: '#e8f5e9', label: 'Low Risk' },
medium: { color: '#f57c00', bgColor: '#fff3e0', label: 'Medium Risk' },
high: { color: '#d84315', bgColor: '#fbe9e7', label: 'High Risk' },
critical: { color: '#c62828', bgColor: '#ffebee', label: 'Critical' },
// Risk level colors
const riskLevelColors: Record<RiskLevel, { color: string; bgColor: string }> = {
low: { color: '#2e7d32', bgColor: '#e8f5e9' },
medium: { color: '#f57c00', bgColor: '#fff3e0' },
high: { color: '#d84315', bgColor: '#fbe9e7' },
critical: { color: '#c62828', bgColor: '#ffebee' },
}
// Schedule status labels
const scheduleStatusLabels: Record<ScheduleStatus, string> = {
on_track: 'On Track',
at_risk: 'At Risk',
delayed: 'Delayed',
// Schedule status translation keys
const scheduleStatusKeys: Record<ScheduleStatus, string> = {
on_track: 'status.onTrack',
at_risk: 'status.atRisk',
delayed: 'status.delayed',
}
// Resource status labels
const resourceStatusLabels: Record<ResourceStatus, string> = {
adequate: 'Adequate',
constrained: 'Constrained',
overloaded: 'Overloaded',
// Resource status translation keys
const resourceStatusKeys: Record<ResourceStatus, string> = {
adequate: 'resourceStatus.adequate',
constrained: 'resourceStatus.constrained',
overloaded: 'resourceStatus.overloaded',
}
export function ProjectHealthCard({ project, onClick }: ProjectHealthCardProps) {
const { t } = useTranslation('health')
const healthColor = getHealthScoreColor(project.health_score)
const riskConfig = riskLevelConfig[project.risk_level]
const riskColors = riskLevelColors[project.risk_level]
const progressPercent = project.task_count > 0
? Math.round((project.completed_task_count / project.task_count) * 100)
: 0
@@ -72,11 +74,11 @@ export function ProjectHealthCard({ project, onClick }: ProjectHealthCardProps)
<div
style={{
...styles.riskBadge,
color: riskConfig.color,
backgroundColor: riskConfig.bgColor,
color: riskColors.color,
backgroundColor: riskColors.bgColor,
}}
>
{riskConfig.label}
{t(`riskLevel.${project.risk_level}`)}
</div>
</div>
@@ -110,25 +112,25 @@ export function ProjectHealthCard({ project, onClick }: ProjectHealthCardProps)
<span style={{ ...styles.scoreValue, color: healthColor }}>
{project.health_score}
</span>
<span style={styles.scoreLabel}>Health</span>
<span style={styles.scoreLabel}>{t('card.health')}</span>
</div>
</div>
<div style={styles.statusSection}>
<div style={styles.statusItem}>
<span style={styles.statusLabel}>Schedule</span>
<span style={styles.statusLabel}>{t('card.schedule')}</span>
<span style={styles.statusValue}>
{scheduleStatusLabels[project.schedule_status]}
{t(scheduleStatusKeys[project.schedule_status])}
</span>
</div>
<div style={styles.statusItem}>
<span style={styles.statusLabel}>Resources</span>
<span style={styles.statusLabel}>{t('card.resources')}</span>
<span style={styles.statusValue}>
{resourceStatusLabels[project.resource_status]}
{t(resourceStatusKeys[project.resource_status])}
</span>
</div>
{project.owner_name && (
<div style={styles.statusItem}>
<span style={styles.statusLabel}>Owner</span>
<span style={styles.statusLabel}>{t('card.owner')}</span>
<span style={styles.statusValue}>{project.owner_name}</span>
</div>
)}
@@ -138,7 +140,7 @@ export function ProjectHealthCard({ project, onClick }: ProjectHealthCardProps)
{/* Task Progress */}
<div style={styles.progressSection}>
<div style={styles.progressHeader}>
<span style={styles.progressLabel}>Task Progress</span>
<span style={styles.progressLabel}>{t('card.taskProgress')}</span>
<span style={styles.progressValue}>
{project.completed_task_count} / {project.task_count}
</span>
@@ -158,17 +160,17 @@ export function ProjectHealthCard({ project, onClick }: ProjectHealthCardProps)
<div style={styles.metricsSection}>
<div style={styles.metricItem}>
<span style={styles.metricValue}>{project.blocker_count}</span>
<span style={styles.metricLabel}>Blockers</span>
<span style={styles.metricLabel}>{t('card.blockers')}</span>
</div>
<div style={styles.metricItem}>
<span style={{ ...styles.metricValue, color: project.overdue_task_count > 0 ? '#f44336' : 'inherit' }}>
{project.overdue_task_count}
</span>
<span style={styles.metricLabel}>Overdue</span>
<span style={styles.metricLabel}>{t('card.overdue')}</span>
</div>
<div style={styles.metricItem}>
<span style={styles.metricValue}>{progressPercent}%</span>
<span style={styles.metricLabel}>Complete</span>
<span style={styles.metricLabel}>{t('card.complete')}</span>
</div>
</div>
</div>