Skip to content
This repository was archived by the owner on Jun 14, 2025. It is now read-only.

Commit 2378b84

Browse files
committed
Subtask system
1 parent 86f34ec commit 2378b84

7 files changed

Lines changed: 657 additions & 135 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { useState } from "react";
2+
import Link from "next/link";
3+
import { api } from "~/utils/api";
4+
import { useSession } from "next-auth/react";
5+
6+
type TaskStatus = "TODO" | "IN_PROGRESS" | "REVIEW" | "COMPLETED";
7+
type TaskPriority = "LOW" | "MEDIUM" | "HIGH" | "URGENT";
8+
9+
interface SubTask {
10+
id: string;
11+
title: string;
12+
status: TaskStatus;
13+
priority?: TaskPriority;
14+
dueDate?: Date | null;
15+
assigneeCount?: number;
16+
}
17+
18+
interface SubtaskManagementProps {
19+
taskId: string;
20+
projectId: string;
21+
subTasks: SubTask[];
22+
onRefresh: () => void;
23+
}
24+
25+
const SubtaskManagement: React.FC<SubtaskManagementProps> = ({
26+
taskId,
27+
projectId,
28+
subTasks,
29+
onRefresh,
30+
}) => {
31+
const { data: session } = useSession();
32+
const [showCreateForm, setShowCreateForm] = useState(false);
33+
const [newSubtaskTitle, setNewSubtaskTitle] = useState("");
34+
const [isCreating, setIsCreating] = useState(false);
35+
36+
const createSubtaskMutation = api.task.create.useMutation({
37+
onSuccess: () => {
38+
setNewSubtaskTitle("");
39+
setShowCreateForm(false);
40+
setIsCreating(false);
41+
onRefresh();
42+
},
43+
onError: (error) => {
44+
console.error("Error creating subtask:", error);
45+
setIsCreating(false);
46+
},
47+
});
48+
49+
const handleCreateSubtask = async (e: React.FormEvent) => {
50+
e.preventDefault();
51+
if (!newSubtaskTitle.trim() || isCreating) return;
52+
53+
setIsCreating(true);
54+
await createSubtaskMutation.mutateAsync({
55+
title: newSubtaskTitle.trim(),
56+
description: "",
57+
projectId,
58+
parentTaskId: taskId,
59+
priority: "MEDIUM",
60+
status: "TODO",
61+
});
62+
};
63+
64+
const getStatusColor = (status: TaskStatus) => {
65+
switch (status) {
66+
case "TODO": return "bg-gray-100 text-gray-800";
67+
case "IN_PROGRESS": return "bg-blue-100 text-blue-800";
68+
case "REVIEW": return "bg-yellow-100 text-yellow-800";
69+
case "COMPLETED": return "bg-green-100 text-green-800";
70+
default: return "bg-gray-100 text-gray-800";
71+
}
72+
};
73+
74+
const getPriorityColor = (priority: TaskPriority) => {
75+
switch (priority) {
76+
case "LOW": return "bg-green-100 text-green-800";
77+
case "MEDIUM": return "bg-yellow-100 text-yellow-800";
78+
case "HIGH": return "bg-orange-100 text-orange-800";
79+
case "URGENT": return "bg-red-100 text-red-800";
80+
default: return "bg-gray-100 text-gray-800";
81+
}
82+
};
83+
84+
const completedSubtasks = subTasks.filter(task => task.status === "COMPLETED").length;
85+
const totalSubtasks = subTasks.length;
86+
const progressPercentage = totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
87+
88+
return (
89+
<div className="card">
90+
<div className="p-6 border-b border-border">
91+
<div className="flex items-center justify-between">
92+
<div>
93+
<h3 className="text-lg font-semibold text-foreground">
94+
Subtasks ({totalSubtasks})
95+
</h3>
96+
{totalSubtasks > 0 && (
97+
<div className="mt-2">
98+
<div className="flex items-center gap-2">
99+
<div className="flex-1 bg-gray-200 rounded-full h-2">
100+
<div
101+
className="bg-green-600 h-2 rounded-full transition-all duration-300"
102+
style={{ width: `${progressPercentage}%` }}
103+
/>
104+
</div>
105+
<span className="text-sm text-muted-foreground">
106+
{completedSubtasks}/{totalSubtasks} completed
107+
</span>
108+
</div>
109+
</div>
110+
)}
111+
</div>
112+
{session && (
113+
<button
114+
onClick={() => setShowCreateForm(!showCreateForm)}
115+
className="btn btn-primary"
116+
>
117+
{showCreateForm ? "Cancel" : "Add Subtask"}
118+
</button>
119+
)}
120+
</div>
121+
</div>
122+
123+
<div className="p-6">
124+
{/* Create Subtask Form */}
125+
{showCreateForm && (
126+
<form onSubmit={handleCreateSubtask} className="mb-6 p-4 border border-border rounded-lg bg-muted/30">
127+
<div className="flex gap-2">
128+
<input
129+
type="text"
130+
value={newSubtaskTitle}
131+
onChange={(e) => setNewSubtaskTitle(e.target.value)}
132+
placeholder="Enter subtask title..."
133+
className="flex-1 input"
134+
autoFocus
135+
disabled={isCreating}
136+
/>
137+
<button
138+
type="submit"
139+
disabled={!newSubtaskTitle.trim() || isCreating}
140+
className="btn btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
141+
>
142+
{isCreating ? "Creating..." : "Create"}
143+
</button>
144+
</div>
145+
</form>
146+
)}
147+
148+
{/* Subtasks List */}
149+
{subTasks.length > 0 ? (
150+
<div className="space-y-3">
151+
{subTasks.map((subtask) => (
152+
<div
153+
key={subtask.id}
154+
className="p-4 border border-border rounded-lg hover:border-primary/50 transition-colors"
155+
>
156+
<div className="flex items-start justify-between">
157+
<div className="flex-1">
158+
<Link
159+
href={`/tasks/${subtask.id}`}
160+
className="text-foreground hover:text-primary transition-colors"
161+
>
162+
<h4 className="font-medium mb-2">{subtask.title}</h4>
163+
</Link>
164+
165+
<div className="flex items-center gap-3 text-sm">
166+
<span className={`px-2 py-1 rounded-full ${getStatusColor(subtask.status)}`}>
167+
{subtask.status.replace('_', ' ')}
168+
</span>
169+
170+
{subtask.priority && (
171+
<span className={`px-2 py-1 rounded-full ${getPriorityColor(subtask.priority)}`}>
172+
{subtask.priority}
173+
</span>
174+
)}
175+
176+
{subtask.dueDate && (
177+
<span className="text-muted-foreground">
178+
Due: {new Date(subtask.dueDate).toLocaleDateString()}
179+
</span>
180+
)}
181+
182+
{subtask.assigneeCount && subtask.assigneeCount > 0 && (
183+
<span className="text-muted-foreground">
184+
{subtask.assigneeCount} assignee{subtask.assigneeCount > 1 ? 's' : ''}
185+
</span>
186+
)}
187+
</div>
188+
</div>
189+
190+
<div className="flex items-center gap-2 ml-4">
191+
<Link
192+
href={`/tasks/${subtask.id}`}
193+
className="text-sm text-primary hover:text-primary/80"
194+
>
195+
View
196+
</Link>
197+
<Link
198+
href={`/tasks/${subtask.id}/edit`}
199+
className="text-sm text-muted-foreground hover:text-foreground"
200+
>
201+
Edit
202+
</Link>
203+
</div>
204+
</div>
205+
</div>
206+
))}
207+
</div>
208+
) : (
209+
<div className="text-center text-muted-foreground py-8">
210+
{showCreateForm ? (
211+
<p>Enter a title above to create your first subtask</p>
212+
) : (
213+
<div>
214+
<p>No subtasks yet</p>
215+
{session && (
216+
<p className="text-sm mt-1">Click &quot;Add Subtask&quot; to break this task into smaller parts</p>
217+
)}
218+
</div>
219+
)}
220+
</div>
221+
)}
222+
</div>
223+
</div>
224+
);
225+
};
226+
227+
export default SubtaskManagement;

0 commit comments

Comments
 (0)