refactor(dev-task): 移除工时记录,改为由开始/完成日期自动计算实际工时
- 状态流转到"开发中"时自动记录 startDate - 实际工时 = startDate → completedAt 的工作日 × 8h - 详情抽屉移除工时记录面板,增加"开发开始日期"展示 - 列表行工时展示同步改为日期计算 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { X, AlertTriangle, Link2, ChevronRight, Clock, User, Tag, Calendar } from 'lucide-react';
|
import { X, AlertTriangle, Link2, ChevronRight, Clock, User, Tag, Calendar, Play } from 'lucide-react';
|
||||||
import { StatusBadge } from './StatusBadge';
|
import { StatusBadge } from './StatusBadge';
|
||||||
import { CategoryChip } from './CategoryChip';
|
import { CategoryChip } from './CategoryChip';
|
||||||
import { WorklogPanel } from './WorklogPanel';
|
|
||||||
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
||||||
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
||||||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||||
import { ALLOWED_TRANSITIONS, DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, formatHours } from '@/lib/dev-task';
|
import { ALLOWED_TRANSITIONS, DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, formatHours, calcActualHoursByDates } from '@/lib/dev-task';
|
||||||
import type { DevTaskStatus } from '@/lib/dev-task';
|
import type { DevTaskStatus } from '@/lib/dev-task';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -153,8 +152,15 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose }: Props) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="h-3 w-3 text-[var(--ink-muted)]" />
|
<Clock className="h-3 w-3 text-[var(--ink-muted)]" />
|
||||||
<span className="text-[var(--ink-muted)]">实际</span>
|
<span className="text-[var(--ink-muted)]">实际</span>
|
||||||
<span className="text-[var(--ink)] font-medium">{task.actualHours > 0 ? `${task.actualHours}h` : '-'}</span>
|
<span className="text-[var(--ink)] font-medium">{task.startDate ? formatHours(calcActualHoursByDates(task.startDate, task.completedAt)) : '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{task.startDate && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Play className="h-3 w-3 text-[var(--ink-muted)]" />
|
||||||
|
<span className="text-[var(--ink-muted)]">开始开发</span>
|
||||||
|
<span className="text-[var(--ink)]">{task.startDate}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{task.dueDate && (
|
{task.dueDate && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-3 w-3 text-[var(--ink-muted)]" />
|
<Calendar className="h-3 w-3 text-[var(--ink-muted)]" />
|
||||||
@@ -194,10 +200,6 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 卡片5:工时记录 */}
|
|
||||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
|
||||||
<WorklogPanel taskId={task.id} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
import { StatusBadge } from './StatusBadge';
|
import { StatusBadge } from './StatusBadge';
|
||||||
import { CategoryChip } from './CategoryChip';
|
import { CategoryChip } from './CategoryChip';
|
||||||
import { formatHours } from '@/lib/dev-task';
|
import { formatHours, calcActualHoursByDates } from '@/lib/dev-task';
|
||||||
import type { DevTask } from '@/lib/dev-task';
|
import type { DevTask } from '@/lib/dev-task';
|
||||||
import type { TaskCategory } from '@/lib/task-category';
|
import type { TaskCategory } from '@/lib/task-category';
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ export function DevTaskRow({ task, category, onClick }: Props) {
|
|||||||
<CategoryChip category={category} />
|
<CategoryChip category={category} />
|
||||||
<StatusBadge status={task.status} />
|
<StatusBadge status={task.status} />
|
||||||
<span className="text-[11px] text-[var(--ink-muted)] w-24 text-right tabular-nums">
|
<span className="text-[11px] text-[var(--ink-muted)] w-24 text-right tabular-nums">
|
||||||
{task.actualHours > 0 && <span className="text-[var(--ink-soft)]">{task.actualHours}h</span>}
|
{task.startDate && <span className="text-[var(--ink-soft)]">{calcActualHoursByDates(task.startDate, task.completedAt)}h</span>}
|
||||||
{task.actualHours > 0 && ' / '}
|
{task.startDate && ' / '}
|
||||||
{formatHours(task.estimateHours).split('(')[0]}
|
{formatHours(task.estimateHours).split('(')[0]}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-[var(--ink-soft)] w-14 text-right truncate">{task.assigneeId}</span>
|
<span className="text-[11px] text-[var(--ink-soft)] w-14 text-right truncate">{task.assigneeId}</span>
|
||||||
|
|||||||
@@ -85,6 +85,22 @@ export function formatHours(hours: number): string {
|
|||||||
return `${hours}h(≈${days}人天)`;
|
return `${hours}h(≈${days}人天)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calcActualHoursByDates(startDate?: string, completedAt?: string): number {
|
||||||
|
if (!startDate) return 0;
|
||||||
|
const end = completedAt || new Date().toISOString().slice(0, 10);
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const endDate = new Date(end);
|
||||||
|
if (isNaN(start.getTime()) || isNaN(endDate.getTime()) || endDate < start) return 0;
|
||||||
|
let workDays = 0;
|
||||||
|
const current = new Date(start);
|
||||||
|
while (current <= endDate) {
|
||||||
|
const day = current.getDay();
|
||||||
|
if (day !== 0 && day !== 6) workDays++;
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
}
|
||||||
|
return workDays * 8;
|
||||||
|
}
|
||||||
|
|
||||||
export function generateTaskNo(existingTasks: DevTask[]): string {
|
export function generateTaskNo(existingTasks: DevTask[]): string {
|
||||||
const maxNum = existingTasks.reduce((max, t) => {
|
const maxNum = existingTasks.reduce((max, t) => {
|
||||||
const num = parseInt(t.taskNo.replace('DEV-', ''), 10);
|
const num = parseInt(t.taskNo.replace('DEV-', ''), 10);
|
||||||
|
|||||||
@@ -77,8 +77,10 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
|||||||
if (!canTransition(task.status, to)) {
|
if (!canTransition(task.status, to)) {
|
||||||
return { ok: false, message: `不允许从「${task.status}」流转到「${to}」` };
|
return { ok: false, message: `不允许从「${task.status}」流转到「${to}」` };
|
||||||
}
|
}
|
||||||
const completedAt = to === 'done' ? new Date().toISOString().slice(0, 10) : task.completedAt;
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
get().updateTask(id, { status: to, completedAt });
|
const startDate = (to === 'in_progress' && !task.startDate) ? today : task.startDate;
|
||||||
|
const completedAt = to === 'done' ? today : task.completedAt;
|
||||||
|
get().updateTask(id, { status: to, startDate, completedAt });
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user