feat: add task sets functionality with CRUD operations and UI integration
- Implemented NewTaskSetPage for creating task sets with templates. - Created TaskSetsPage for listing and filtering task sets. - Enhanced TaskTemplatesPage with navigation to task sets. - Updated ProjectTaskForm to support task set selection. - Modified PageHeader to support multiple action buttons. - Initialized database with task_sets and task_set_templates tables. - Added queries for task sets including creation, retrieval, and deletion. - Implemented applyTaskSetToProject function for bulk task creation. - Added test script for verifying task sets functionality.
This commit is contained in:
35
src/app/api/task-sets/[id]/apply/route.js
Normal file
35
src/app/api/task-sets/[id]/apply/route.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { applyTaskSetToProject } from "@/lib/queries/tasks";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withUserAuth } from "@/lib/middleware/auth";
|
||||
|
||||
// POST: Apply a task set to a project (bulk create project tasks)
|
||||
async function applyTaskSetHandler(req, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { project_id } = await req.json();
|
||||
|
||||
if (!project_id) {
|
||||
return NextResponse.json(
|
||||
{ error: "project_id is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const createdTaskIds = applyTaskSetToProject(id, project_id, req.user?.id || null);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Task set applied successfully. Created ${createdTaskIds.length} tasks.`,
|
||||
createdTaskIds
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error applying task set:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to apply task set", details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected route - require authentication
|
||||
export const POST = withUserAuth(applyTaskSetHandler);
|
||||
130
src/app/api/task-sets/[id]/route.js
Normal file
130
src/app/api/task-sets/[id]/route.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
getTaskSetById,
|
||||
updateTaskSet,
|
||||
deleteTaskSet,
|
||||
addTaskTemplateToSet,
|
||||
removeTaskTemplateFromSet,
|
||||
} from "@/lib/queries/tasks";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
|
||||
import initializeDatabase from "@/lib/init-db";
|
||||
|
||||
// GET: Get a specific task set with its templates
|
||||
async function getTaskSetHandler(req, { params }) {
|
||||
initializeDatabase();
|
||||
try {
|
||||
const { id } = await params;
|
||||
const taskSet = getTaskSetById(id);
|
||||
|
||||
if (!taskSet) {
|
||||
return NextResponse.json(
|
||||
{ error: "Task set not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(taskSet);
|
||||
} catch (error) {
|
||||
console.error("Error fetching task set:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch task set" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT: Update a task set
|
||||
async function updateTaskSetHandler(req, { params }) {
|
||||
initializeDatabase();
|
||||
try {
|
||||
const { id } = await params;
|
||||
const updates = await req.json();
|
||||
|
||||
// Validate required fields
|
||||
if (updates.name !== undefined && !updates.name.trim()) {
|
||||
return NextResponse.json(
|
||||
{ error: "Name cannot be empty" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (updates.task_category !== undefined) {
|
||||
const validTypes = ["design", "construction"];
|
||||
if (!validTypes.includes(updates.task_category)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid task_category. Must be one of: design, construction" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle template updates
|
||||
if (updates.templates !== undefined) {
|
||||
// Clear existing templates
|
||||
// Note: This is a simple implementation. In a real app, you might want to handle this more efficiently
|
||||
const currentSet = getTaskSetById(id);
|
||||
if (currentSet) {
|
||||
for (const template of currentSet.templates) {
|
||||
removeTaskTemplateFromSet(id, template.task_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new templates
|
||||
if (Array.isArray(updates.templates)) {
|
||||
for (let i = 0; i < updates.templates.length; i++) {
|
||||
const template = updates.templates[i];
|
||||
addTaskTemplateToSet(id, template.task_id, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove templates from updates object so it doesn't interfere with task set update
|
||||
delete updates.templates;
|
||||
}
|
||||
|
||||
const result = updateTaskSet(id, updates);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Task set not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error updating task set:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update task set", details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: Delete a task set
|
||||
async function deleteTaskSetHandler(req, { params }) {
|
||||
initializeDatabase();
|
||||
try {
|
||||
const { id } = await params;
|
||||
const result = deleteTaskSet(id);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Task set not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting task set:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete task set", details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected routes - require authentication
|
||||
export const GET = withReadAuth(getTaskSetHandler);
|
||||
export const PUT = withUserAuth(updateTaskSetHandler);
|
||||
export const DELETE = withUserAuth(deleteTaskSetHandler);
|
||||
60
src/app/api/task-sets/route.js
Normal file
60
src/app/api/task-sets/route.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
getAllTaskSets,
|
||||
getTaskSetsByProjectType,
|
||||
createTaskSet,
|
||||
} from "@/lib/queries/tasks";
|
||||
import { NextResponse } from "next/server";
|
||||
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
|
||||
import initializeDatabase from "@/lib/init-db";
|
||||
|
||||
// GET: Get all task sets or filter by task category
|
||||
async function getTaskSetsHandler(req) {
|
||||
initializeDatabase();
|
||||
const { searchParams } = new URL(req.url);
|
||||
const taskCategory = searchParams.get("task_category");
|
||||
|
||||
if (taskCategory) {
|
||||
const taskSets = getTaskSetsByTaskCategory(taskCategory);
|
||||
return NextResponse.json(taskSets);
|
||||
} else {
|
||||
const taskSets = getAllTaskSets();
|
||||
return NextResponse.json(taskSets);
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Create a new task set
|
||||
async function createTaskSetHandler(req) {
|
||||
initializeDatabase();
|
||||
try {
|
||||
const data = await req.json();
|
||||
|
||||
if (!data.name || !data.task_category) {
|
||||
return NextResponse.json(
|
||||
{ error: "Name and task_category are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate task_category
|
||||
const validTypes = ["design", "construction"];
|
||||
if (!validTypes.includes(data.task_category)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid task_category. Must be one of: design, construction" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const setId = createTaskSet(data);
|
||||
return NextResponse.json({ success: true, id: setId });
|
||||
} catch (error) {
|
||||
console.error("Error creating task set:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create task set", details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Protected routes - require authentication
|
||||
export const GET = withReadAuth(getTaskSetsHandler);
|
||||
export const POST = withUserAuth(createTaskSetHandler);
|
||||
@@ -10,8 +10,8 @@ export default function ProjectTasksPage() {
|
||||
<PageHeader
|
||||
title="Zadania"
|
||||
description="Zarządzaj zadaniami projektów"
|
||||
action={
|
||||
<Link href="/tasks/templates">
|
||||
actions={[
|
||||
<Link href="/tasks/templates" key="templates">
|
||||
<Button variant="secondary" size="md">
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
@@ -28,8 +28,26 @@ export default function ProjectTasksPage() {
|
||||
</svg>
|
||||
Szablony zadań
|
||||
</Button>
|
||||
</Link>,
|
||||
<Link href="/task-sets" key="task-sets">
|
||||
<Button variant="secondary" size="md">
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||
/>
|
||||
</svg>
|
||||
Zestawy zadań
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<ProjectTasksList />
|
||||
</PageContainer>
|
||||
|
||||
245
src/app/task-sets/[id]/apply/page.js
Normal file
245
src/app/task-sets/[id]/apply/page.js
Normal file
@@ -0,0 +1,245 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import PageContainer from "@/components/ui/PageContainer";
|
||||
import PageHeader from "@/components/ui/PageHeader";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
export default function ApplyTaskSetPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const setId = params.id;
|
||||
|
||||
const [taskSet, setTaskSet] = useState(null);
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [selectedProject, setSelectedProject] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch task set
|
||||
const setResponse = await fetch(`/api/task-sets/${setId}`);
|
||||
if (setResponse.ok) {
|
||||
const setData = await setResponse.json();
|
||||
setTaskSet(setData);
|
||||
} else {
|
||||
console.error('Failed to fetch task set');
|
||||
router.push('/task-sets');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch projects
|
||||
const projectsResponse = await fetch('/api/projects');
|
||||
if (projectsResponse.ok) {
|
||||
const projectsData = await projectsResponse.json();
|
||||
setProjects(projectsData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (setId) {
|
||||
fetchData();
|
||||
}
|
||||
}, [setId, router]);
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!selectedProject) {
|
||||
alert("Wybierz projekt");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApplying(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/task-sets/${setId}/apply`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project_id: parseInt(selectedProject) })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert(`Zestaw zadań został pomyślnie zastosowany. Utworzono ${result.createdTaskIds.length} zadań.`);
|
||||
router.push(`/projects/${selectedProject}`);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Błąd: ${error.details || 'Nie udało się zastosować zestawu zadań'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying task set:', error);
|
||||
alert('Wystąpił błąd podczas stosowania zestawu zadań');
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader title="Zastosuj zestaw zadań" />
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-gray-500">Ładowanie...</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!taskSet) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader title="Zastosuj zestaw zadań" />
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500">Zestaw zadań nie został znaleziony</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Zastosuj zestaw zadań"
|
||||
description={`Zastosuj zestaw "${taskSet.name}" do projektu`}
|
||||
/>
|
||||
|
||||
<div className="max-w-4xl">
|
||||
{/* Task set info */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Informacje o zestawie</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{taskSet.name}</div>
|
||||
{taskSet.description && (
|
||||
<div className="text-sm text-gray-600 mt-1">{taskSet.description}</div>
|
||||
)}
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
Typ projektu: <span className="capitalize">{taskSet.project_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-2">
|
||||
Zawarte szablony zadań ({taskSet.templates?.length || 0}):
|
||||
</div>
|
||||
{taskSet.templates && taskSet.templates.length > 0 ? (
|
||||
<ul className="text-sm text-gray-600 space-y-1">
|
||||
{taskSet.templates.map((template, index) => (
|
||||
<li key={template.task_id} className="flex items-center">
|
||||
<span className="text-gray-400 mr-2">{index + 1}.</span>
|
||||
{template.name}
|
||||
{template.max_wait_days > 0 && (
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
({template.max_wait_days} dni)
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Brak szablonów w zestawie</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Project selection */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Wybierz projekt</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Wybierz projekt, do którego chcesz zastosować ten zestaw zadań
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Projekt *
|
||||
</label>
|
||||
<select
|
||||
value={selectedProject}
|
||||
onChange={(e) => setSelectedProject(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
>
|
||||
<option value="">Wybierz projekt...</option>
|
||||
{projects.map((project) => (
|
||||
<option key={project.project_id} value={project.project_id}>
|
||||
{project.project_name} - {project.city} ({project.project_type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{projects.length === 0 && (
|
||||
<p className="text-gray-500 text-sm">
|
||||
Brak dostępnych projektów dla tego typu zestawu zadań.
|
||||
{taskSet.project_type !== 'design+construction' &&
|
||||
" Spróbuj utworzyć projekt typu 'Projekt + Budowa' lub zmienić typ zestawu."
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Warning */}
|
||||
<Card className="mb-6 bg-yellow-50 border-yellow-200">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-yellow-800">
|
||||
Informacja
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-yellow-700">
|
||||
<p>
|
||||
Zastosowanie tego zestawu utworzy {taskSet.templates?.length || 0} nowych zadań w wybranym projekcie.
|
||||
Zadania będą miały status "Oczekujące" i zostaną przypisane zgodnie z domyślnymi regułami.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => router.back()}
|
||||
disabled={isApplying}
|
||||
>
|
||||
Anuluj
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleApply}
|
||||
disabled={isApplying || !selectedProject}
|
||||
>
|
||||
{isApplying ? "Stosowanie..." : "Zastosuj zestaw"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
354
src/app/task-sets/[id]/page.js
Normal file
354
src/app/task-sets/[id]/page.js
Normal file
@@ -0,0 +1,354 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import PageContainer from "@/components/ui/PageContainer";
|
||||
import PageHeader from "@/components/ui/PageHeader";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
export default function EditTaskSetPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const setId = params.id;
|
||||
|
||||
const [taskTemplates, setTaskTemplates] = useState([]);
|
||||
const [taskSet, setTaskSet] = useState(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
project_type: "design",
|
||||
selectedTemplates: []
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch task set
|
||||
const setResponse = await fetch(`/api/task-sets/${setId}`);
|
||||
if (setResponse.ok) {
|
||||
const setData = await setResponse.json();
|
||||
setTaskSet(setData);
|
||||
setFormData({
|
||||
name: setData.name,
|
||||
description: setData.description || "",
|
||||
project_type: setData.project_type,
|
||||
selectedTemplates: setData.templates?.map(t => t.task_id) || []
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to fetch task set');
|
||||
router.push('/task-sets');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch available task templates
|
||||
const templatesResponse = await fetch('/api/tasks/templates');
|
||||
if (templatesResponse.ok) {
|
||||
const templatesData = await templatesResponse.json();
|
||||
setTaskTemplates(templatesData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (setId) {
|
||||
fetchData();
|
||||
}
|
||||
}, [setId, router]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
alert("Nazwa zestawu jest wymagana");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.selectedTemplates.length === 0) {
|
||||
alert("Wybierz przynajmniej jeden szablon zadania");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Update the task set
|
||||
const updateData = {
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
task_category: formData.task_category,
|
||||
templates: formData.selectedTemplates.map((templateId, index) => ({
|
||||
task_id: templateId,
|
||||
sort_order: index
|
||||
}))
|
||||
};
|
||||
|
||||
const response = await fetch(`/api/task-sets/${setId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update task set');
|
||||
}
|
||||
|
||||
router.push('/task-sets');
|
||||
} catch (error) {
|
||||
console.error('Error updating task set:', error);
|
||||
alert('Wystąpił błąd podczas aktualizacji zestawu zadań');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Czy na pewno chcesz usunąć ten zestaw zadań?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/task-sets/${setId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
router.push('/task-sets');
|
||||
} else {
|
||||
alert('Wystąpił błąd podczas usuwania zestawu zadań');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting task set:', error);
|
||||
alert('Wystąpił błąd podczas usuwania zestawu zadań');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTemplate = (templateId) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
selectedTemplates: prev.selectedTemplates.includes(templateId)
|
||||
? prev.selectedTemplates.filter(id => id !== templateId)
|
||||
: [...prev.selectedTemplates, templateId]
|
||||
}));
|
||||
};
|
||||
|
||||
const moveTemplate = (fromIndex, toIndex) => {
|
||||
const newSelected = [...formData.selectedTemplates];
|
||||
const [moved] = newSelected.splice(fromIndex, 1);
|
||||
newSelected.splice(toIndex, 0, moved);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
selectedTemplates: newSelected
|
||||
}));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader title="Edycja zestawu zadań" />
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-gray-500">Ładowanie...</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!taskSet) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader title="Edycja zestawu zadań" />
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500">Zestaw zadań nie został znaleziony</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Edycja zestawu zadań"
|
||||
description={`Edytuj zestaw: ${taskSet.name}`}
|
||||
/>
|
||||
|
||||
<form onSubmit={handleSubmit} className="max-w-4xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Basic info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Informacje podstawowe</h3>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nazwa zestawu *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="np. Standardowe zadania projektowe"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Opis
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Opcjonalny opis zestawu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kategoria zadań *
|
||||
</label>
|
||||
<select
|
||||
value={formData.task_category}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, task_category: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
>
|
||||
<option value="design">Zadania projektowe</option>
|
||||
<option value="construction">Zadania budowlane</option>
|
||||
</select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Template selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Wybrane szablony zadań</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Wybrano: {formData.selectedTemplates.length} szablonów
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formData.selectedTemplates.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{formData.selectedTemplates.map((templateId, index) => {
|
||||
const template = taskTemplates.find(t => t.task_id === templateId);
|
||||
return (
|
||||
<div key={templateId} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-gray-600">{index + 1}.</span>
|
||||
<span className="text-sm">{template?.name || 'Nieznany szablon'}</span>
|
||||
</div>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => index > 0 && moveTemplate(index, index - 1)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
disabled={index === 0}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => index < formData.selectedTemplates.length - 1 && moveTemplate(index, index + 1)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
disabled={index === formData.selectedTemplates.length - 1}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleTemplate(templateId)}
|
||||
className="text-red-400 hover:text-red-600"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">Brak wybranych szablonów</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Available templates */}
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Dostępne szablony zadań</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Wybierz szablony, które chcesz dodać do zestawu
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{taskTemplates.map((template) => (
|
||||
<label key={template.task_id} className="flex items-center space-x-2 p-2 hover:bg-gray-50 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.selectedTemplates.includes(template.task_id)}
|
||||
onChange={() => toggleTemplate(template.task_id)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{template.name}</div>
|
||||
{template.description && (
|
||||
<div className="text-xs text-gray-600">{template.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{taskTemplates.length === 0 && (
|
||||
<p className="text-gray-500 text-sm text-center py-4">
|
||||
Brak dostępnych szablonów zadań. Najpierw utwórz szablony w zakładce "Szablony zadań".
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-between mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
onClick={handleDelete}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Usuń zestaw
|
||||
</Button>
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => router.back()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Anuluj
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Zapisywanie..." : "Zapisz zmiany"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
289
src/app/task-sets/new/page.js
Normal file
289
src/app/task-sets/new/page.js
Normal file
@@ -0,0 +1,289 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import PageContainer from "@/components/ui/PageContainer";
|
||||
import PageHeader from "@/components/ui/PageHeader";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
export default function NewTaskSetPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [taskTemplates, setTaskTemplates] = useState([]);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
project_type: "design",
|
||||
selectedTemplates: []
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch available task templates
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tasks/templates');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTaskTemplates(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching templates:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTemplates();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
alert("Nazwa zestawu jest wymagana");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.selectedTemplates.length === 0) {
|
||||
alert("Wybierz przynajmniej jeden szablon zadania");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Create the task set
|
||||
const createResponse = await fetch('/api/task-sets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
project_type: formData.project_type
|
||||
})
|
||||
});
|
||||
|
||||
if (!createResponse.ok) {
|
||||
throw new Error('Failed to create task set');
|
||||
}
|
||||
|
||||
const { id: setId } = await createResponse.json();
|
||||
|
||||
// Add templates to the set
|
||||
const templatesData = {
|
||||
templates: formData.selectedTemplates.map((templateId, index) => ({
|
||||
task_id: templateId,
|
||||
sort_order: index
|
||||
}))
|
||||
};
|
||||
|
||||
const updateResponse = await fetch(`/api/task-sets/${setId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(templatesData)
|
||||
});
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
throw new Error('Failed to add templates to task set');
|
||||
}
|
||||
|
||||
router.push('/task-sets');
|
||||
} catch (error) {
|
||||
console.error('Error creating task set:', error);
|
||||
alert('Wystąpił błąd podczas tworzenia zestawu zadań');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTemplate = (templateId) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
selectedTemplates: prev.selectedTemplates.includes(templateId)
|
||||
? prev.selectedTemplates.filter(id => id !== templateId)
|
||||
: [...prev.selectedTemplates, templateId]
|
||||
}));
|
||||
};
|
||||
|
||||
const moveTemplate = (fromIndex, toIndex) => {
|
||||
const newSelected = [...formData.selectedTemplates];
|
||||
const [moved] = newSelected.splice(fromIndex, 1);
|
||||
newSelected.splice(toIndex, 0, moved);
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
selectedTemplates: newSelected
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Nowy zestaw zadań"
|
||||
description="Utwórz nowy zestaw zadań z szablonów"
|
||||
/>
|
||||
|
||||
<form onSubmit={handleSubmit} className="max-w-4xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Basic info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Informacje podstawowe</h3>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nazwa zestawu *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="np. Standardowe zadania projektowe"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Opis
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="Opcjonalny opis zestawu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kategoria zadań *
|
||||
</label>
|
||||
<select
|
||||
value={formData.task_category}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, task_category: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
>
|
||||
<option value="design">Zadania projektowe</option>
|
||||
<option value="construction">Zadania budowlane</option>
|
||||
</select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Template selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Wybrane szablony zadań</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Wybrano: {formData.selectedTemplates.length} szablonów
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formData.selectedTemplates.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{formData.selectedTemplates.map((templateId, index) => {
|
||||
const template = taskTemplates.find(t => t.task_id === templateId);
|
||||
return (
|
||||
<div key={templateId} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-gray-600">{index + 1}.</span>
|
||||
<span className="text-sm">{template?.name || 'Nieznany szablon'}</span>
|
||||
</div>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => index > 0 && moveTemplate(index, index - 1)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
disabled={index === 0}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => index < formData.selectedTemplates.length - 1 && moveTemplate(index, index + 1)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
disabled={index === formData.selectedTemplates.length - 1}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleTemplate(templateId)}
|
||||
className="text-red-400 hover:text-red-600"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">Brak wybranych szablonów</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Available templates */}
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold">Dostępne szablony zadań</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Wybierz szablony, które chcesz dodać do zestawu
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{taskTemplates.map((template) => (
|
||||
<label key={template.task_id} className="flex items-center space-x-2 p-2 hover:bg-gray-50 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.selectedTemplates.includes(template.task_id)}
|
||||
onChange={() => toggleTemplate(template.task_id)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{template.name}</div>
|
||||
{template.description && (
|
||||
<div className="text-xs text-gray-600">{template.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{taskTemplates.length === 0 && (
|
||||
<p className="text-gray-500 text-sm text-center py-4">
|
||||
Brak dostępnych szablonów zadań. Najpierw utwórz szablony w zakładce "Szablony zadań".
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end space-x-4 mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => router.back()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Anuluj
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Tworzenie..." : "Utwórz zestaw"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
189
src/app/task-sets/page.js
Normal file
189
src/app/task-sets/page.js
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import Badge from "@/components/ui/Badge";
|
||||
import PageContainer from "@/components/ui/PageContainer";
|
||||
import PageHeader from "@/components/ui/PageHeader";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
export default function TaskSetsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [taskSets, setTaskSets] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTaskSets = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/task-sets');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTaskSets(data);
|
||||
} else {
|
||||
console.error('Failed to fetch task sets');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching task sets:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTaskSets();
|
||||
}, []);
|
||||
|
||||
const filteredTaskSets = taskSets.filter(taskSet => {
|
||||
if (filter === "all") return true;
|
||||
return taskSet.task_category === filter;
|
||||
});
|
||||
|
||||
const getTaskCategoryBadge = (taskCategory) => {
|
||||
const colors = {
|
||||
design: "bg-blue-100 text-blue-800",
|
||||
construction: "bg-green-100 text-green-800"
|
||||
};
|
||||
|
||||
return (
|
||||
<Badge className={colors[taskCategory] || "bg-gray-100 text-gray-800"}>
|
||||
{taskCategory === "design" ? "Zadania projektowe" : taskCategory === "construction" ? "Zadania budowlane" : taskCategory}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Zestawy zadań"
|
||||
description="Zarządzaj zestawami zadań"
|
||||
/>
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="text-gray-500">Ładowanie...</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Zestawy zadań"
|
||||
description="Zarządzaj zestawami zadań"
|
||||
action={
|
||||
<Link href="/task-sets/new">
|
||||
<Button variant="primary" size="lg">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Nowy zestaw
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Filter buttons */}
|
||||
<div className="mb-6">
|
||||
<div className="flex space-x-2">
|
||||
{["all", "design", "construction"].map(type => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={filter === type ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => setFilter(type)}
|
||||
>
|
||||
{type === "all" ? "Wszystkie" :
|
||||
type === "design" ? "Projektowanie" :
|
||||
"Budowa"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task sets grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredTaskSets.map((taskSet) => (
|
||||
<Card key={taskSet.set_id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{taskSet.name}
|
||||
</h3>
|
||||
{taskSet.description && (
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{taskSet.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{getTaskCategoryBadge(taskSet.task_category)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">Szablony zadań:</span>{" "}
|
||||
{taskSet.templates?.length || 0}
|
||||
</div>
|
||||
|
||||
{taskSet.templates && taskSet.templates.length > 0 && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{taskSet.templates.slice(0, 3).map((template) => (
|
||||
<li key={template.task_id}>{template.name}</li>
|
||||
))}
|
||||
{taskSet.templates.length > 3 && (
|
||||
<li>...i {taskSet.templates.length - 3} więcej</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-2 pt-2">
|
||||
<Link href={`/task-sets/${taskSet.set_id}`}>
|
||||
<Button variant="secondary" size="sm">
|
||||
Edytuj
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/task-sets/${taskSet.set_id}/apply`}>
|
||||
<Button variant="primary" size="sm">
|
||||
Zastosuj
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredTaskSets.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500 mb-4">
|
||||
{filter === "all"
|
||||
? "Brak zestawów zadań"
|
||||
: `Brak zestawów zadań dla typu "${filter}"`
|
||||
}
|
||||
</div>
|
||||
<Link href="/task-sets/new">
|
||||
<Button variant="primary">
|
||||
Utwórz pierwszy zestaw
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -40,8 +40,8 @@ export default function TaskTemplatesPage() {
|
||||
<PageHeader
|
||||
title={t('taskTemplates.title')}
|
||||
description={t('taskTemplates.subtitle')}
|
||||
action={
|
||||
<Link href="/tasks/templates/new">
|
||||
actions={[
|
||||
<Link href="/tasks/templates/new" key="new-template">
|
||||
<Button variant="primary" size="lg">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
@@ -58,8 +58,26 @@ export default function TaskTemplatesPage() {
|
||||
</svg>
|
||||
{t('taskTemplates.newTemplate')}
|
||||
</Button>
|
||||
</Link>,
|
||||
<Link href="/task-sets" key="task-sets">
|
||||
<Button variant="secondary" size="lg">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||
/>
|
||||
</svg>
|
||||
Zestawy zadań
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-500">{t('common.loading')}</div>
|
||||
@@ -73,8 +91,8 @@ export default function TaskTemplatesPage() {
|
||||
<PageHeader
|
||||
title={t('taskTemplates.title')}
|
||||
description={t('taskTemplates.subtitle')}
|
||||
action={
|
||||
<Link href="/tasks/templates/new">
|
||||
actions={[
|
||||
<Link href="/tasks/templates/new" key="new-template">
|
||||
<Button variant="primary" size="lg">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
@@ -91,8 +109,26 @@ export default function TaskTemplatesPage() {
|
||||
</svg>
|
||||
{t('taskTemplates.newTemplate')}
|
||||
</Button>
|
||||
</Link>,
|
||||
<Link href="/task-sets" key="task-sets">
|
||||
<Button variant="secondary" size="lg">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||
/>
|
||||
</svg>
|
||||
Zestawy zadań
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{templates.length === 0 ? (
|
||||
|
||||
@@ -8,9 +8,12 @@ import { useTranslation } from "@/lib/i18n";
|
||||
export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
const { t } = useTranslation();
|
||||
const [taskTemplates, setTaskTemplates] = useState([]);
|
||||
const [taskSets, setTaskSets] = useState([]);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [taskType, setTaskType] = useState("template"); // "template" or "custom"
|
||||
const [project, setProject] = useState(null);
|
||||
const [taskType, setTaskType] = useState("template"); // "template", "custom", or "task_set"
|
||||
const [selectedTemplate, setSelectedTemplate] = useState("");
|
||||
const [selectedTaskSet, setSelectedTaskSet] = useState("");
|
||||
const [customTaskName, setCustomTaskName] = useState("");
|
||||
const [customMaxWaitDays, setCustomMaxWaitDays] = useState("");
|
||||
const [customDescription, setCustomDescription] = useState("");
|
||||
@@ -19,6 +22,11 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch project details
|
||||
fetch(`/api/projects/${projectId}`)
|
||||
.then((res) => res.json())
|
||||
.then(setProject);
|
||||
|
||||
// Fetch available task templates
|
||||
fetch("/api/tasks/templates")
|
||||
.then((res) => res.json())
|
||||
@@ -28,7 +36,23 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
fetch("/api/project-tasks/users")
|
||||
.then((res) => res.json())
|
||||
.then(setUsers);
|
||||
}, []);
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch task sets when project type is available
|
||||
if (project?.project_type) {
|
||||
let apiUrl = '/api/task-sets';
|
||||
if (project.project_type === 'design+construction') {
|
||||
// For design+construction projects, don't filter - show all task sets
|
||||
// User can choose which type to apply
|
||||
} else {
|
||||
apiUrl += `?project_type=${project.project_type}`;
|
||||
}
|
||||
fetch(apiUrl)
|
||||
.then((res) => res.json())
|
||||
.then(setTaskSets);
|
||||
}
|
||||
}, [project?.project_type]);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
@@ -36,40 +60,61 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
// Validate based on task type
|
||||
if (taskType === "template" && !selectedTemplate) return;
|
||||
if (taskType === "custom" && !customTaskName.trim()) return;
|
||||
if (taskType === "task_set" && !selectedTaskSet) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
project_id: parseInt(projectId),
|
||||
priority,
|
||||
assigned_to: assignedTo || null,
|
||||
};
|
||||
if (taskType === "task_set") {
|
||||
// Apply task set
|
||||
const response = await fetch(`/api/task-sets/${selectedTaskSet}/apply`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ project_id: parseInt(projectId) }),
|
||||
});
|
||||
|
||||
if (taskType === "template") {
|
||||
requestData.task_template_id = parseInt(selectedTemplate);
|
||||
if (response.ok) {
|
||||
// Reset form
|
||||
setSelectedTaskSet("");
|
||||
setPriority("normal");
|
||||
setAssignedTo("");
|
||||
if (onTaskAdded) onTaskAdded();
|
||||
} else {
|
||||
alert(t("tasks.addTaskError"));
|
||||
}
|
||||
} else {
|
||||
requestData.custom_task_name = customTaskName.trim();
|
||||
requestData.custom_max_wait_days = parseInt(customMaxWaitDays) || 0;
|
||||
requestData.custom_description = customDescription.trim();
|
||||
}
|
||||
// Create single task
|
||||
const requestData = {
|
||||
project_id: parseInt(projectId),
|
||||
priority,
|
||||
assigned_to: assignedTo || null,
|
||||
};
|
||||
|
||||
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("");
|
||||
setCustomDescription("");
|
||||
setPriority("normal");
|
||||
setAssignedTo("");
|
||||
if (onTaskAdded) onTaskAdded();
|
||||
} else {
|
||||
alert(t("tasks.addTaskError"));
|
||||
if (taskType === "template") {
|
||||
requestData.task_template_id = parseInt(selectedTemplate);
|
||||
} else {
|
||||
requestData.custom_task_name = customTaskName.trim();
|
||||
requestData.custom_max_wait_days = parseInt(customMaxWaitDays) || 0;
|
||||
requestData.custom_description = customDescription.trim();
|
||||
}
|
||||
|
||||
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("");
|
||||
setCustomDescription("");
|
||||
setPriority("normal");
|
||||
setAssignedTo("");
|
||||
if (onTaskAdded) onTaskAdded();
|
||||
} else {
|
||||
alert(t("tasks.addTaskError"));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert(t("tasks.addTaskError"));
|
||||
@@ -83,7 +128,7 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
{t("tasks.taskType")}
|
||||
</label>
|
||||
<div className="flex space-x-6">
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -94,6 +139,16 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
/>
|
||||
<span className="ml-2 text-sm text-gray-900">{t("tasks.fromTemplate")}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
value="task_set"
|
||||
checked={taskType === "task_set"}
|
||||
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">Z zestawu zadań</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -126,6 +181,41 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : taskType === "task_set" ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Wybierz zestaw zadań
|
||||
</label>
|
||||
<select
|
||||
value={selectedTaskSet}
|
||||
onChange={(e) => setSelectedTaskSet(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="">Wybierz zestaw...</option>
|
||||
{taskSets.map((taskSet) => (
|
||||
<option key={taskSet.set_id} value={taskSet.set_id}>
|
||||
{taskSet.name} ({taskSet.task_category === 'design' ? 'Zadania projektowe' : 'Zadania budowlane'}) - {taskSet.templates?.length || 0} zadań
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedTaskSet && (
|
||||
<div className="mt-2 p-3 bg-blue-50 rounded-md">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Podgląd zestawu:</strong>
|
||||
</p>
|
||||
<ul className="text-sm text-blue-700 mt-1 list-disc list-inside">
|
||||
{taskSets
|
||||
.find(ts => ts.set_id === parseInt(selectedTaskSet))
|
||||
?.templates?.map((template, index) => (
|
||||
<li key={template.task_id}>
|
||||
{template.name} ({template.max_wait_days} dni)
|
||||
</li>
|
||||
)) || []}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -169,39 +259,43 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t("tasks.assignedTo")} <span className="text-gray-500 text-xs">({t("common.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="">{t("projects.unassigned")}</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.name} ({user.email})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{taskType !== "task_set" && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t("tasks.assignedTo")} <span className="text-gray-500 text-xs">({t("common.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="">{t("projects.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">
|
||||
{t("tasks.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">{t("tasks.low")}</option>
|
||||
<option value="normal">{t("tasks.normal")}</option>
|
||||
<option value="high">{t("tasks.high")}</option>
|
||||
<option value="urgent">{t("tasks.urgent")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t("tasks.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">{t("tasks.low")}</option>
|
||||
<option value="normal">{t("tasks.normal")}</option>
|
||||
<option value="high">{t("tasks.high")}</option>
|
||||
<option value="urgent">{t("tasks.urgent")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
@@ -210,10 +304,11 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
(taskType === "template" && !selectedTemplate) ||
|
||||
(taskType === "custom" && !customTaskName.trim())
|
||||
(taskType === "custom" && !customTaskName.trim()) ||
|
||||
(taskType === "task_set" && !selectedTaskSet)
|
||||
}
|
||||
>
|
||||
{isSubmitting ? t("tasks.adding") : t("tasks.addTask")}
|
||||
{isSubmitting ? t("tasks.adding") : taskType === "task_set" ? "Zastosuj zestaw" : t("tasks.addTask")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
"use client";
|
||||
|
||||
const PageHeader = ({ title, description, children, action, className = "" }) => {
|
||||
const PageHeader = ({ title, description, children, action, actions, className = "" }) => {
|
||||
return (
|
||||
<div className={`flex justify-between items-start mb-8 ${className}`}>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">{title}</h1>
|
||||
{description && <p className="text-gray-600 dark:text-gray-400 mt-1">{description}</p>}
|
||||
</div>
|
||||
{(children || action) && (
|
||||
<div className="ml-6 flex-shrink-0">{action || children}</div>
|
||||
{(children || action || actions) && (
|
||||
<div className="ml-6 flex-shrink-0">
|
||||
{actions ? (
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
{actions.map((actionItem, index) => (
|
||||
<div key={index}>{actionItem}</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
action || children
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,7 +41,29 @@ export default function initializeDatabase() {
|
||||
name TEXT NOT NULL,
|
||||
max_wait_days INTEGER DEFAULT 0,
|
||||
is_standard INTEGER NOT NULL DEFAULT 0
|
||||
); -- Table: project_tasks
|
||||
);
|
||||
|
||||
-- Table: task_sets
|
||||
CREATE TABLE IF NOT EXISTS task_sets (
|
||||
set_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
task_category TEXT CHECK(task_category IN ('design', 'construction')) NOT NULL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Table: task_set_templates
|
||||
CREATE TABLE IF NOT EXISTS task_set_templates (
|
||||
set_id INTEGER NOT NULL,
|
||||
task_template_id INTEGER NOT NULL,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (set_id, task_template_id),
|
||||
FOREIGN KEY (set_id) REFERENCES task_sets(set_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (task_template_id) REFERENCES tasks(task_id)
|
||||
);
|
||||
|
||||
-- Table: project_tasks
|
||||
CREATE TABLE IF NOT EXISTS project_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
@@ -342,6 +364,24 @@ export default function initializeDatabase() {
|
||||
console.warn("Migration warning:", e.message);
|
||||
}
|
||||
|
||||
// Migration: Rename project_type to task_category in task_sets
|
||||
try {
|
||||
// Check if the old column exists and rename it
|
||||
const tableInfo = db.prepare("PRAGMA table_info(task_sets)").all();
|
||||
const hasOldColumn = tableInfo.some(col => col.name === 'project_type');
|
||||
const hasNewColumn = tableInfo.some(col => col.name === 'task_category');
|
||||
|
||||
if (hasOldColumn && !hasNewColumn) {
|
||||
// Rename the column
|
||||
db.exec(`
|
||||
ALTER TABLE task_sets RENAME COLUMN project_type TO task_category;
|
||||
`);
|
||||
console.log("✅ Renamed project_type to task_category in task_sets");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Migration warning:", e.message);
|
||||
}
|
||||
|
||||
// Generic file attachments table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS file_attachments (
|
||||
|
||||
@@ -413,3 +413,192 @@ export function updateProjectTask(taskId, updates, userId = null) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== TASK SETS =====
|
||||
|
||||
// Get all task sets
|
||||
export function getAllTaskSets() {
|
||||
const taskSets = db
|
||||
.prepare("SELECT * FROM task_sets ORDER BY name ASC")
|
||||
.all();
|
||||
|
||||
// Add templates to each task set
|
||||
return taskSets.map(taskSet => {
|
||||
const templates = db
|
||||
.prepare(`
|
||||
SELECT
|
||||
tst.sort_order,
|
||||
t.task_id,
|
||||
t.name,
|
||||
t.max_wait_days,
|
||||
t.description
|
||||
FROM task_set_templates tst
|
||||
JOIN tasks t ON tst.task_template_id = t.task_id
|
||||
WHERE tst.set_id = ?
|
||||
ORDER BY tst.sort_order ASC
|
||||
`)
|
||||
.all(taskSet.set_id);
|
||||
|
||||
return { ...taskSet, templates };
|
||||
});
|
||||
}
|
||||
|
||||
// Get task sets by task category
|
||||
export function getTaskSetsByTaskCategory(taskCategory) {
|
||||
const taskSets = db
|
||||
.prepare("SELECT * FROM task_sets WHERE task_category = ? ORDER BY name ASC")
|
||||
.all(taskCategory);
|
||||
|
||||
// Add templates to each task set
|
||||
return taskSets.map(taskSet => {
|
||||
const templates = db
|
||||
.prepare(`
|
||||
SELECT
|
||||
tst.sort_order,
|
||||
t.task_id,
|
||||
t.name,
|
||||
t.max_wait_days,
|
||||
t.description
|
||||
FROM task_set_templates tst
|
||||
JOIN tasks t ON tst.task_template_id = t.task_id
|
||||
WHERE tst.set_id = ?
|
||||
ORDER BY tst.sort_order ASC
|
||||
`)
|
||||
.all(taskSet.set_id);
|
||||
|
||||
return { ...taskSet, templates };
|
||||
});
|
||||
}
|
||||
|
||||
// Get task set by ID with templates
|
||||
export function getTaskSetById(setId) {
|
||||
const taskSet = db
|
||||
.prepare("SELECT * FROM task_sets WHERE set_id = ?")
|
||||
.get(setId);
|
||||
|
||||
if (taskSet) {
|
||||
const templates = db
|
||||
.prepare(`
|
||||
SELECT
|
||||
tst.sort_order,
|
||||
t.task_id,
|
||||
t.name,
|
||||
t.max_wait_days,
|
||||
t.description
|
||||
FROM task_set_templates tst
|
||||
JOIN tasks t ON tst.task_template_id = t.task_id
|
||||
WHERE tst.set_id = ?
|
||||
ORDER BY tst.sort_order ASC
|
||||
`)
|
||||
.all(setId);
|
||||
|
||||
return { ...taskSet, templates };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a new task set
|
||||
export function createTaskSet(data) {
|
||||
const result = db
|
||||
.prepare(`
|
||||
INSERT INTO task_sets (name, description, task_category, created_at, updated_at)
|
||||
VALUES (?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))
|
||||
`)
|
||||
.run(data.name, data.description || null, data.task_category);
|
||||
|
||||
return result.lastInsertRowid;
|
||||
}
|
||||
|
||||
// Update a task set
|
||||
export function updateTaskSet(setId, data) {
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.name !== undefined) {
|
||||
fields.push("name = ?");
|
||||
values.push(data.name);
|
||||
}
|
||||
|
||||
if (data.description !== undefined) {
|
||||
fields.push("description = ?");
|
||||
values.push(data.description || null);
|
||||
}
|
||||
|
||||
if (data.task_category !== undefined) {
|
||||
fields.push("task_category = ?");
|
||||
values.push(data.task_category);
|
||||
}
|
||||
|
||||
fields.push("updated_at = datetime('now', 'localtime')");
|
||||
values.push(setId);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE task_sets
|
||||
SET ${fields.join(", ")}
|
||||
WHERE set_id = ?
|
||||
`);
|
||||
|
||||
return stmt.run(...values);
|
||||
}
|
||||
|
||||
// Delete a task set
|
||||
export function deleteTaskSet(setId) {
|
||||
// Delete task set templates first (cascade should handle this, but being explicit)
|
||||
db.prepare("DELETE FROM task_set_templates WHERE set_id = ?").run(setId);
|
||||
|
||||
// Delete the task set
|
||||
return db.prepare("DELETE FROM task_sets WHERE set_id = ?").run(setId);
|
||||
}
|
||||
|
||||
// Add task template to set
|
||||
export function addTaskTemplateToSet(setId, taskTemplateId, sortOrder = 0) {
|
||||
return db
|
||||
.prepare(`
|
||||
INSERT OR REPLACE INTO task_set_templates (set_id, task_template_id, sort_order)
|
||||
VALUES (?, ?, ?)
|
||||
`)
|
||||
.run(setId, taskTemplateId, sortOrder);
|
||||
}
|
||||
|
||||
// Remove task template from set
|
||||
export function removeTaskTemplateFromSet(setId, taskTemplateId) {
|
||||
return db
|
||||
.prepare(`
|
||||
DELETE FROM task_set_templates
|
||||
WHERE set_id = ? AND task_template_id = ?
|
||||
`)
|
||||
.run(setId, taskTemplateId);
|
||||
}
|
||||
|
||||
// Apply task set to project (bulk create project tasks)
|
||||
export function applyTaskSetToProject(setId, projectId, userId = null) {
|
||||
// Get the task set with templates
|
||||
const taskSet = getTaskSetById(setId);
|
||||
if (!taskSet) {
|
||||
throw new Error(`Task set with ID ${setId} not found`);
|
||||
}
|
||||
|
||||
const createdTasks = [];
|
||||
const language = getUserLanguage();
|
||||
|
||||
// Create project tasks for each template in the set
|
||||
for (const template of taskSet.templates) {
|
||||
const result = createProjectTask({
|
||||
project_id: projectId,
|
||||
task_template_id: template.task_id,
|
||||
status: "pending",
|
||||
priority: "normal",
|
||||
created_by: userId,
|
||||
assigned_to: null, // Will be assigned based on user role logic in createProjectTask
|
||||
});
|
||||
|
||||
createdTasks.push(result.lastInsertRowid);
|
||||
|
||||
// Add system note for task set application
|
||||
const logMessage = `${serverT("Task added from set", language)} "${taskSet.name}"`;
|
||||
addNoteToTask(result.lastInsertRowid, logMessage, true, userId);
|
||||
}
|
||||
|
||||
return createdTasks;
|
||||
}
|
||||
|
||||
71
test-task-sets.mjs
Normal file
71
test-task-sets.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Test script to verify task sets functionality
|
||||
import { getAllTaskSets, createTaskSet } from './src/lib/queries/tasks.js';
|
||||
import initializeDatabase from './src/lib/init-db.js';
|
||||
|
||||
async function testTaskSets() {
|
||||
console.log('Testing Task Sets Database Functions...\n');
|
||||
|
||||
try {
|
||||
// Initialize database
|
||||
initializeDatabase();
|
||||
|
||||
// Test 1: Get all task sets
|
||||
console.log('1. Getting all task sets...');
|
||||
const taskSets = getAllTaskSets();
|
||||
console.log(`Found ${taskSets.length} task sets:`);
|
||||
taskSets.forEach(set => {
|
||||
console.log(` - ${set.name} (${set.task_category}):`, JSON.stringify(set));
|
||||
});
|
||||
|
||||
// Test 2: Create a new task set (design)
|
||||
console.log('\n2. Creating design task set...');
|
||||
const designSetId = createTaskSet({
|
||||
name: 'Test Design Set',
|
||||
description: 'Test task set for design tasks',
|
||||
task_category: 'design',
|
||||
templates: []
|
||||
});
|
||||
console.log(`Created task set with ID: ${designSetId}`);
|
||||
|
||||
// Test 3: Create a construction task set
|
||||
console.log('\n3. Creating construction task set...');
|
||||
const constructionSetId = createTaskSet({
|
||||
name: 'Test Construction Set',
|
||||
description: 'Test task set for construction tasks',
|
||||
task_category: 'construction',
|
||||
templates: []
|
||||
});
|
||||
console.log(`Created task set with ID: ${constructionSetId}`);
|
||||
|
||||
// Test 4: Try to create invalid task set (should fail)
|
||||
console.log('\n4. Testing invalid task category (should fail)...');
|
||||
try {
|
||||
const invalidSetId = createTaskSet({
|
||||
name: 'Invalid Set',
|
||||
description: 'This should fail',
|
||||
task_category: 'design+construction',
|
||||
templates: []
|
||||
});
|
||||
console.log('✗ Should have failed to create invalid task set');
|
||||
} catch (error) {
|
||||
console.log('✓ Correctly rejected invalid task category:', error.message);
|
||||
}
|
||||
|
||||
// Test 5: Get all task sets again
|
||||
console.log('\n5. Getting all task sets after creation...');
|
||||
const updatedTaskSets = getAllTaskSets();
|
||||
console.log(`Found ${updatedTaskSets.length} task sets:`);
|
||||
updatedTaskSets.forEach(set => {
|
||||
console.log(` - ${set.name} (${set.task_category})`);
|
||||
});
|
||||
|
||||
console.log('\n✅ All tests passed! Task sets functionality is working correctly.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testTaskSets();
|
||||
Reference in New Issue
Block a user