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:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user