- Added translation support for task-related strings in ProjectTaskForm and ProjectTasksSection components. - Integrated translation for navigation items in the Navigation component. - Created ProjectCalendarWidget component with Polish translations for project statuses and deadlines. - Developed Tooltip component for enhanced user experience with tooltips. - Established a field change history logging system in the database with associated queries. - Enhanced task update logging to include translated status and priority changes. - Introduced server-side translations for system messages to improve localization.
222 lines
6.9 KiB
JavaScript
222 lines
6.9 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Button from "./ui/Button";
|
|
import Badge from "./ui/Badge";
|
|
import { useTranslation } from "@/lib/i18n";
|
|
|
|
export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
|
const { t } = useTranslation();
|
|
const [taskTemplates, setTaskTemplates] = useState([]);
|
|
const [users, setUsers] = useState([]);
|
|
const [taskType, setTaskType] = useState("template"); // "template" or "custom"
|
|
const [selectedTemplate, setSelectedTemplate] = useState("");
|
|
const [customTaskName, setCustomTaskName] = useState("");
|
|
const [customMaxWaitDays, setCustomMaxWaitDays] = useState("");
|
|
const [customDescription, setCustomDescription] = useState("");
|
|
const [priority, setPriority] = useState("normal");
|
|
const [assignedTo, setAssignedTo] = useState("");
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Fetch available task templates
|
|
fetch("/api/tasks/templates")
|
|
.then((res) => res.json())
|
|
.then(setTaskTemplates);
|
|
|
|
// Fetch users for assignment
|
|
fetch("/api/project-tasks/users")
|
|
.then((res) => res.json())
|
|
.then(setUsers);
|
|
}, []);
|
|
|
|
async function handleSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
// Validate based on task type
|
|
if (taskType === "template" && !selectedTemplate) return;
|
|
if (taskType === "custom" && !customTaskName.trim()) return;
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
const requestData = {
|
|
project_id: parseInt(projectId),
|
|
priority,
|
|
assigned_to: assignedTo || null,
|
|
};
|
|
|
|
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"));
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
{t("tasks.taskType")}
|
|
</label>
|
|
<div className="flex space-x-6">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
value="template"
|
|
checked={taskType === "template"}
|
|
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">{t("tasks.fromTemplate")}</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="radio"
|
|
value="custom"
|
|
checked={taskType === "custom"}
|
|
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">{t("tasks.customTask")}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{taskType === "template" ? (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t("tasks.selectTemplate")}
|
|
</label>{" "}
|
|
<select
|
|
value={selectedTemplate}
|
|
onChange={(e) => setSelectedTemplate(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="">{t("tasks.chooseTemplate")}</option>
|
|
{taskTemplates.map((template) => (
|
|
<option key={template.task_id} value={template.task_id}>
|
|
{template.name} ({template.max_wait_days} {t("tasks.days")})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t("tasks.taskName")}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={customTaskName}
|
|
onChange={(e) => setCustomTaskName(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"
|
|
placeholder={t("tasks.enterTaskName")}
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t("tasks.maxWait")}
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={customMaxWaitDays}
|
|
onChange={(e) => setCustomMaxWaitDays(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"
|
|
placeholder="0"
|
|
min="0"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t("tasks.description")}
|
|
</label>
|
|
<textarea
|
|
value={customDescription}
|
|
onChange={(e) => setCustomDescription(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"
|
|
placeholder={t("tasks.enterDescription")}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</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>
|
|
|
|
<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
|
|
type="submit"
|
|
variant="primary"
|
|
disabled={
|
|
isSubmitting ||
|
|
(taskType === "template" && !selectedTemplate) ||
|
|
(taskType === "custom" && !customTaskName.trim())
|
|
}
|
|
>
|
|
{isSubmitting ? t("tasks.adding") : t("tasks.addTask")}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|