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

189
src/app/task-sets/page.js Normal file
View 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>
);
}