feat: Add NoteForm, ProjectForm, and ProjectTaskForm components

- Implemented NoteForm for adding notes to projects.
- Created ProjectForm for managing project details with contract selection.
- Developed ProjectTaskForm for adding tasks to projects, supporting both templates and custom tasks.

feat: Add ProjectTasksSection component

- Introduced ProjectTasksSection to display and manage tasks for a specific project.
- Includes functionality for adding, updating, and deleting tasks.

feat: Create TaskTemplateForm for managing task templates

- Added TaskTemplateForm for creating new task templates with required wait days.

feat: Implement UI components

- Created reusable UI components: Badge, Button, Card, Input, Loading, Navigation.
- Enhanced user experience with consistent styling and functionality.

feat: Set up database and queries

- Initialized SQLite database with tables for contracts, projects, tasks, project tasks, and notes.
- Implemented queries for managing contracts, projects, tasks, and notes.

chore: Add error handling and loading states

- Improved error handling in forms and data fetching.
- Added loading states for better user feedback during data operations.
This commit is contained in:
Chop
2025-06-02 22:07:05 +02:00
parent aa1eb99ce9
commit d0586f2876
43 changed files with 3272 additions and 137 deletions

View File

@@ -0,0 +1,178 @@
"use client";
import { useState, useEffect } from "react";
import Button from "./ui/Button";
import Badge from "./ui/Badge";
export default function ProjectTaskForm({ projectId, onTaskAdded }) {
const [taskTemplates, setTaskTemplates] = useState([]);
const [taskType, setTaskType] = useState("template"); // "template" or "custom"
const [selectedTemplate, setSelectedTemplate] = useState("");
const [customTaskName, setCustomTaskName] = useState("");
const [customMaxWaitDays, setCustomMaxWaitDays] = useState("");
const [priority, setPriority] = useState("normal");
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
// Fetch available task templates
fetch("/api/tasks/templates")
.then((res) => res.json())
.then(setTaskTemplates);
}, []);
async function handleSubmit(e) {
e.preventDefault();
// Validate based on task type
if (taskType === "template" && !selectedTemplate) return;
if (taskType === "custom" && !customTaskName.trim()) return;
setIsSubmitting(true);
try {
const requestData = {
project_id: parseInt(projectId),
priority,
};
if (taskType === "template") {
requestData.task_template_id = parseInt(selectedTemplate);
} else {
requestData.custom_task_name = customTaskName.trim();
requestData.custom_max_wait_days = parseInt(customMaxWaitDays) || 0;
}
const res = await fetch("/api/project-tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestData),
});
if (res.ok) {
// Reset form
setSelectedTemplate("");
setCustomTaskName("");
setCustomMaxWaitDays("");
setPriority("normal");
if (onTaskAdded) onTaskAdded();
} else {
alert("Failed to add task to project.");
}
} catch (error) {
alert("Error adding task to project.");
} finally {
setIsSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Task Type
</label>
<div className="flex space-x-6">
<label className="flex items-center">
<input
type="radio"
value="template"
checked={taskType === "template"}
onChange={(e) => setTaskType(e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="ml-2 text-sm text-gray-900">From Template</span>
</label>
<label className="flex items-center">
<input
type="radio"
value="custom"
checked={taskType === "custom"}
onChange={(e) => setTaskType(e.target.value)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<span className="ml-2 text-sm text-gray-900">Custom Task</span>
</label>
</div>
</div>
{taskType === "template" ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Task Template
</label>{" "}
<select
value={selectedTemplate}
onChange={(e) => setSelectedTemplate(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"
required
>
<option value="">Choose a task template...</option>
{taskTemplates.map((template) => (
<option key={template.task_id} value={template.task_id}>
{template.name} ({template.max_wait_days} days)
</option>
))}
</select>
</div>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Task Name
</label>
<input
type="text"
value={customTaskName}
onChange={(e) => setCustomTaskName(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"
placeholder="Enter custom task name..."
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Max Wait Days
</label>
<input
type="number"
value={customMaxWaitDays}
onChange={(e) => setCustomMaxWaitDays(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"
placeholder="0"
min="0"
/>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Priority
</label>
<select
value={priority}
onChange={(e) => setPriority(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="low">Low</option>
<option value="normal">Normal</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div className="flex justify-end">
<Button
type="submit"
variant="primary"
disabled={
isSubmitting ||
(taskType === "template" && !selectedTemplate) ||
(taskType === "custom" && !customTaskName.trim())
}
>
{isSubmitting ? "Adding..." : "Add Task"}
</Button>
</div>
</form>
);
}