Skip to content

Commit edfbd23

Browse files
author
赵汝波
committed
Implement task priority and due date features in task management
- Added `TaskPriority` enum to define task priority levels: none, low, medium, high, and urgent. - Introduced `dueDate` field in the Task model to allow setting deadlines for tasks. - Updated task creation and update actions to handle priority and due date inputs. - Enhanced UI components to display and manage task priority and due date effectively. - Updated project plan to reflect the addition of task priority and due date functionalities.
1 parent 15aa46f commit edfbd23

17 files changed

Lines changed: 476 additions & 56 deletions

File tree

PROJECT_PLAN.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,7 @@
214214
- [x] Vitest 覆盖 ACL 规则(8 项通过)
215215
- [x] 对标 Linear:`dateFrom`/`dateTo` + 筛选 Chips + `@@index([userId, updatedAt])`
216216
- [x] Vitest:`date-query` 日历与区间(parse / normalize / filter)
217+
- [x] 任务优先级(`TaskPriority`)+ 截止日期(`dueDate`),列表/详情/新建/命令面板展示,URL `priority=` 筛选,排序 `due_asc` / `due_desc`
218+
- [x] `/tasks` 快捷键 `C`:打开新建任务 Sheet 并保留当前筛选参数
219+
- [x] Prisma 索引:`@@index([userId, dueDate])`
217220

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- CreateEnum
2+
CREATE TYPE "TaskPriority" AS ENUM ('none', 'low', 'medium', 'high', 'urgent');
3+
4+
-- AlterTable
5+
ALTER TABLE "Task" ADD COLUMN "priority" "TaskPriority" NOT NULL DEFAULT 'none',
6+
ADD COLUMN "dueDate" TIMESTAMP(3);
7+
8+
-- CreateIndex
9+
CREATE INDEX "Task_userId_dueDate_idx" ON "Task"("userId", "dueDate");

prisma/schema.prisma

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ enum TaskStatus {
1414
done
1515
}
1616

17+
enum TaskPriority {
18+
none
19+
low
20+
medium
21+
high
22+
urgent
23+
}
24+
1725
model User {
1826
id String @id @default(cuid())
1927
email String @unique
@@ -36,16 +44,19 @@ model Project {
3644
}
3745

3846
model Task {
39-
id String @id @default(cuid())
47+
id String @id @default(cuid())
4048
title String
4149
description String?
42-
status TaskStatus @default(todo)
50+
status TaskStatus @default(todo)
51+
priority TaskPriority @default(none)
52+
dueDate DateTime?
4353
userId String
44-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
54+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
4555
projectId String?
46-
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
47-
createdAt DateTime @default(now())
48-
updatedAt DateTime @updatedAt
56+
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
57+
createdAt DateTime @default(now())
58+
updatedAt DateTime @updatedAt
4959
5060
@@index([userId, updatedAt])
61+
@@index([userId, dueDate])
5162
}

src/app/actions/tasks.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { revalidatePath } from 'next/cache'
44
import { auth } from '@/auth'
55
import { prisma } from '@/lib/db'
66
import { assertCan, can } from '@/lib/acl'
7+
import { dueDateFromYmd } from '@/lib/due-date'
78
import { createTaskSchema, updateTaskSchema } from '@/lib/validations/task'
9+
import type { TaskPriority } from '@/types/task'
810

911
function revalidateTaskViews(projectId?: string | null) {
1012
revalidatePath('/tasks')
@@ -15,6 +17,17 @@ function revalidateTaskViews(projectId?: string | null) {
1517
}
1618
}
1719

20+
function parseDueForUpdate(
21+
raw: FormDataEntryValue | null
22+
): Date | null | undefined {
23+
if (raw === null) return undefined
24+
if (typeof raw !== 'string') return undefined
25+
const t = raw.trim()
26+
if (t === '') return null
27+
const d = dueDateFromYmd(t)
28+
return d === null ? undefined : d
29+
}
30+
1831
export async function createTaskAction(formData: FormData) {
1932
const session = await auth()
2033
const userId = session?.user?.id
@@ -26,6 +39,8 @@ export async function createTaskAction(formData: FormData) {
2639
title: formData.get('title'),
2740
description: formData.get('description') ?? '',
2841
status: formData.get('status') ?? undefined,
42+
priority: formData.get('priority') ?? undefined,
43+
dueDate: formData.get('dueDate') ?? '',
2944
})
3045

3146
if (!parsed.success) {
@@ -35,7 +50,9 @@ export async function createTaskAction(formData: FormData) {
3550
}
3651
}
3752

38-
const { title, description, status } = parsed.data
53+
const { title, description, status, priority, dueDate: dueYmd } = parsed.data
54+
const dueDateResolved =
55+
dueYmd && dueYmd.trim() ? (dueDateFromYmd(dueYmd) ?? null) : null
3956
const projectIdRaw = formData.get('projectId')
4057
const projectId =
4158
typeof projectIdRaw === 'string' && projectIdRaw.length > 0
@@ -60,6 +77,8 @@ export async function createTaskAction(formData: FormData) {
6077
title,
6178
description: description?.trim() ? description.trim() : null,
6279
status: status ?? 'todo',
80+
priority: priority ?? ('none' as TaskPriority),
81+
dueDate: dueDateResolved,
6382
userId: userId!,
6483
projectId,
6584
},
@@ -114,13 +133,15 @@ export async function updateTaskAction(formData: FormData) {
114133
title: formData.get('title') ?? undefined,
115134
description: formData.get('description') ?? '',
116135
status: formData.get('status') ?? undefined,
136+
priority: formData.get('priority') ?? undefined,
137+
dueDate: formData.get('dueDate') ?? '',
117138
})
118139

119140
if (!parsed.success) {
120141
return { ok: false as const, error: '校验失败' }
121142
}
122143

123-
const { id, title, description, status } = parsed.data
144+
const { id, title, description, status, priority } = parsed.data
124145

125146
const existing = await prisma.task.findUnique({
126147
where: { id },
@@ -136,16 +157,24 @@ export async function updateTaskAction(formData: FormData) {
136157
})
137158
if (!gate.ok) return { ok: false as const, error: gate.error }
138159

160+
const dueParsed = parseDueForUpdate(formData.get('dueDate'))
161+
139162
const data: {
140163
title?: string
141164
description?: string | null
142165
status?: 'todo' | 'doing' | 'done'
166+
priority?: TaskPriority
167+
dueDate?: Date | null
143168
} = {}
144169
if (title !== undefined) data.title = title
145170
if (description !== undefined) {
146171
data.description = description.trim() ? description.trim() : null
147172
}
148173
if (status !== undefined) data.status = status
174+
if (priority !== undefined) data.priority = priority
175+
if (dueParsed !== undefined) {
176+
data.dueDate = dueParsed
177+
}
149178

150179
await prisma.task.update({
151180
where: { id },

src/app/projects/[id]/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export default async function ProjectDetailPage({
4848
title: t.title,
4949
description: t.description,
5050
status: t.status,
51+
priority: t.priority,
52+
dueDate: t.dueDate?.toISOString() ?? null,
5153
projectId: t.projectId,
5254
project: t.project,
5355
}));
@@ -82,6 +84,7 @@ export default async function ProjectDetailPage({
8284
sort: "updated_desc",
8385
dateFrom: "",
8486
dateTo: "",
87+
priority: "all",
8588
}}
8689
/>
8790
</main>

src/app/tasks/page.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TasksView } from "@/components/tasks-view";
44
import { normalizeDateRange, parseYmdParam } from "@/lib/date-query";
55
import { prisma } from "@/lib/db";
66
import { getTasksForUser, type TaskQuery, type TaskSort } from "@/lib/tasks-data";
7-
import type { TaskListItem, TaskStatus } from "@/types/task";
7+
import type { TaskListItem, TaskPriority, TaskStatus } from "@/types/task";
88
import { redirect } from "next/navigation";
99
import { Suspense } from "react";
1010

@@ -14,6 +14,17 @@ const ALLOWED_SORT: TaskSort[] = [
1414
"updated_asc",
1515
"created_desc",
1616
"created_asc",
17+
"due_asc",
18+
"due_desc",
19+
];
20+
21+
const ALLOWED_PRIORITY: (TaskPriority | "all")[] = [
22+
"all",
23+
"none",
24+
"low",
25+
"medium",
26+
"high",
27+
"urgent",
1728
];
1829

1930
function pickFirst(value: string | string[] | undefined): string | undefined {
@@ -45,7 +56,13 @@ function parseTaskQuery(
4556
const rawTo = parseYmdParam(pickFirst(sp.dateTo));
4657
const { dateFrom, dateTo } = normalizeDateRange(rawFrom, rawTo);
4758

48-
return { keyword, status, projectId, sort, dateFrom, dateTo };
59+
const rawPriority = pickFirst(sp.priority);
60+
const priority =
61+
rawPriority && ALLOWED_PRIORITY.includes(rawPriority as TaskPriority | "all")
62+
? (rawPriority as TaskQuery["priority"])
63+
: undefined;
64+
65+
return { keyword, status, projectId, sort, dateFrom, dateTo, priority };
4966
}
5067

5168
export default async function TasksPage({
@@ -76,6 +93,8 @@ export default async function TasksPage({
7693
title: t.title,
7794
description: t.description,
7895
status: t.status,
96+
priority: t.priority,
97+
dueDate: t.dueDate?.toISOString() ?? null,
7998
projectId: t.projectId,
8099
project: t.project,
81100
}));
@@ -91,7 +110,7 @@ export default async function TasksPage({
91110
我的任务
92111
</h1>
93112
<p className="mt-2 text-sm text-zinc-500">
94-
关键词、状态、项目、排序与「更新时间」日期范围;条件写入 URL,刷新不丢。
113+
关键词、状态、优先级、项目、排序与日期;条件写入 URL,刷新不丢。
95114
</p>
96115
</div>
97116
<CreateTaskSheet projects={projects} />
@@ -115,6 +134,7 @@ export default async function TasksPage({
115134
sort: query.sort ?? "updated_desc",
116135
dateFrom: query.dateFrom ?? "",
117136
dateTo: query.dateTo ?? "",
137+
priority: (query.priority as string) ?? "all",
118138
}}
119139
/>
120140
</Suspense>

src/components/shell/command-menu.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
DialogDescription,
2222
DialogTitle,
2323
} from '@/components/ui/dialog'
24+
import { PRIORITY_LABEL } from '@/lib/task-priority'
2425
import type { TaskPaletteHit } from '@/types/task'
2526

2627
interface SidebarProject {
@@ -173,8 +174,24 @@ export function CommandMenu({ projects }: CommandMenuProps) {
173174
<Search className='h-4 w-4 text-violet-500' />
174175
}
175176
right={
176-
<span className='flex shrink-0 items-center gap-2 text-[11px] text-zinc-400'>
177+
<span className='flex max-w-[11rem] shrink-0 flex-wrap items-center justify-end gap-x-2 gap-y-0.5 text-[11px] text-zinc-400'>
178+
{t.priority !== 'none' ? (
179+
<span className='text-violet-600'>
180+
{PRIORITY_LABEL[t.priority]}
181+
</span>
182+
) : null}
177183
<span>{STATUS_LABEL[t.status]}</span>
184+
{t.dueDate ? (
185+
<span className='font-mono text-zinc-500'>
186+
{new Date(t.dueDate).toLocaleDateString(
187+
'zh-CN',
188+
{
189+
month: 'numeric',
190+
day: 'numeric',
191+
},
192+
)}
193+
</span>
194+
) : null}
178195
{t.project ? (
179196
<span className='max-w-[7rem] truncate'>
180197
{t.project.name}

0 commit comments

Comments
 (0)