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:
2025-10-07 21:58:08 +02:00
parent e19172d2bb
commit 952caf10d1
14 changed files with 1838 additions and 77 deletions

View 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>
);
}