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 ? (
|
||||
|
||||
Reference in New Issue
Block a user