feat: Add user tracking to project tasks and notes
- Implemented user tracking columns in project_tasks and notes tables. - Added created_by and assigned_to fields to project_tasks. - Introduced created_by field and is_system flag in notes. - Updated API endpoints to handle user tracking during task and note creation. - Enhanced database initialization to include new columns and indexes. - Created utility functions to fetch users for task assignment. - Updated front-end components to display user information for tasks and notes. - Added tests for project-tasks API endpoints to verify functionality.
This commit is contained in:
@@ -6,12 +6,14 @@ import Badge from "./ui/Badge";
|
||||
|
||||
export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
const [taskTemplates, setTaskTemplates] = useState([]);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [taskType, setTaskType] = useState("template"); // "template" or "custom"
|
||||
const [selectedTemplate, setSelectedTemplate] = useState("");
|
||||
const [customTaskName, setCustomTaskName] = useState("");
|
||||
const [customMaxWaitDays, setCustomMaxWaitDays] = useState("");
|
||||
const [customDescription, setCustomDescription] = useState("");
|
||||
const [priority, setPriority] = useState("normal");
|
||||
const [assignedTo, setAssignedTo] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -19,6 +21,11 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
fetch("/api/tasks/templates")
|
||||
.then((res) => res.json())
|
||||
.then(setTaskTemplates);
|
||||
|
||||
// Fetch users for assignment
|
||||
fetch("/api/project-tasks/users")
|
||||
.then((res) => res.json())
|
||||
.then(setUsers);
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
@@ -34,6 +41,7 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
const requestData = {
|
||||
project_id: parseInt(projectId),
|
||||
priority,
|
||||
assigned_to: assignedTo || null,
|
||||
};
|
||||
|
||||
if (taskType === "template") {
|
||||
@@ -56,6 +64,7 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
setCustomMaxWaitDays("");
|
||||
setCustomDescription("");
|
||||
setPriority("normal");
|
||||
setAssignedTo("");
|
||||
if (onTaskAdded) onTaskAdded();
|
||||
} else {
|
||||
alert("Failed to add task to project.");
|
||||
@@ -158,6 +167,24 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Assign To <span className="text-gray-500 text-xs">(optional)</span>
|
||||
</label>
|
||||
<select
|
||||
value={assignedTo}
|
||||
onChange={(e) => setAssignedTo(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.name} ({user.email})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Priority
|
||||
|
||||
@@ -273,6 +273,28 @@ export default function ProjectTasksList() {
|
||||
<td className="px-4 py-3 text-sm text-gray-600">
|
||||
{task.address || "N/A"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">
|
||||
{task.created_by_name ? (
|
||||
<div>
|
||||
<div className="font-medium">{task.created_by_name}</div>
|
||||
<div className="text-xs text-gray-500">{task.created_by_email}</div>
|
||||
</div>
|
||||
) : (
|
||||
"N/A"
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">
|
||||
{task.assigned_to_name ? (
|
||||
<div>
|
||||
<div className="font-medium">{task.assigned_to_name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{task.assigned_to_email}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400 italic">Unassigned</span>
|
||||
)}
|
||||
</td>
|
||||
{showTimeLeft && (
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -361,7 +383,7 @@ export default function ProjectTasksList() {
|
||||
const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => {
|
||||
const filteredTasks = filterTasks(tasks);
|
||||
const groupedTasks = groupTasksByName(filteredTasks);
|
||||
const colSpan = showTimeLeft ? "8" : "7";
|
||||
const colSpan = showTimeLeft ? "10" : "9";
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -379,7 +401,13 @@ export default function ProjectTasksList() {
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||
Address
|
||||
</th>{" "}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||
Created By
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||
Assigned To
|
||||
</th>
|
||||
{showTimeLeft && (
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||
Time Left
|
||||
|
||||
@@ -517,6 +517,11 @@ export default function ProjectTasksSection({ projectId }) {
|
||||
System
|
||||
</span>
|
||||
)}
|
||||
{note.created_by_name && (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded-full font-medium">
|
||||
{note.created_by_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-800">
|
||||
{note.note}
|
||||
@@ -525,6 +530,11 @@ export default function ProjectTasksSection({ projectId }) {
|
||||
{formatDate(note.note_date, {
|
||||
includeTime: true,
|
||||
})}
|
||||
{note.created_by_name && (
|
||||
<span className="ml-2">
|
||||
by {note.created_by_name}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{!note.is_system && (
|
||||
|
||||
Reference in New Issue
Block a user