372 lines
12 KiB
JavaScript
372 lines
12 KiB
JavaScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import Button from "./ui/Button";
|
||
import Badge from "./ui/Badge";
|
||
import { formatDate } from "@/lib/utils";
|
||
import { formatDistanceToNow, parseISO } from "date-fns";
|
||
import { useTranslation } from "@/lib/i18n";
|
||
|
||
export default function TaskCommentsModal({ task, isOpen, onClose }) {
|
||
const { t } = useTranslation();
|
||
const [notes, setNotes] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [newNote, setNewNote] = useState("");
|
||
const [loadingAdd, setLoadingAdd] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (isOpen && task) {
|
||
fetchNotes();
|
||
}
|
||
}, [isOpen, task]);
|
||
|
||
const fetchNotes = async () => {
|
||
if (!task?.id) return;
|
||
|
||
try {
|
||
setLoading(true);
|
||
const res = await fetch(`/api/task-notes?task_id=${task.id}`);
|
||
const data = await res.json();
|
||
setNotes(data);
|
||
} catch (error) {
|
||
console.error("Failed to fetch notes:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleAddNote = async () => {
|
||
if (!newNote.trim() || !task?.id) return;
|
||
|
||
try {
|
||
setLoadingAdd(true);
|
||
const res = await fetch("/api/task-notes", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
task_id: task.id,
|
||
note: newNote.trim(),
|
||
}),
|
||
});
|
||
|
||
if (res.ok) {
|
||
setNewNote("");
|
||
await fetchNotes(); // Refresh notes
|
||
} else {
|
||
alert("Failed to add note");
|
||
}
|
||
} catch (error) {
|
||
alert("Error adding note");
|
||
} finally {
|
||
setLoadingAdd(false);
|
||
}
|
||
};
|
||
|
||
const handleDeleteNote = async (noteId) => {
|
||
if (!confirm("Are you sure you want to delete this note?")) return;
|
||
|
||
try {
|
||
const res = await fetch("/api/task-notes", {
|
||
method: "DELETE",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ note_id: noteId }),
|
||
});
|
||
|
||
if (res.ok) {
|
||
await fetchNotes(); // Refresh notes
|
||
} else {
|
||
alert("Failed to delete note");
|
||
}
|
||
} catch (error) {
|
||
alert("Error deleting note");
|
||
}
|
||
};
|
||
|
||
const handleKeyDown = (e) => {
|
||
if (e.key === "Escape") {
|
||
onClose();
|
||
} else if (e.key === "Enter" && e.ctrlKey) {
|
||
handleAddNote();
|
||
}
|
||
};
|
||
|
||
const getPriorityVariant = (priority) => {
|
||
switch (priority) {
|
||
case "urgent":
|
||
return "urgent";
|
||
case "high":
|
||
return "high";
|
||
case "normal":
|
||
return "normal";
|
||
case "low":
|
||
return "low";
|
||
default:
|
||
return "default";
|
||
}
|
||
};
|
||
|
||
const getStatusVariant = (status) => {
|
||
switch (status) {
|
||
case "completed":
|
||
case "cancelled":
|
||
return "success";
|
||
case "in_progress":
|
||
return "secondary";
|
||
case "pending":
|
||
return "primary";
|
||
default:
|
||
return "default";
|
||
}
|
||
};
|
||
|
||
const formatTaskDate = (dateString, label) => {
|
||
if (!dateString) return null;
|
||
try {
|
||
const date = dateString.includes("T") ? parseISO(dateString) : new Date(dateString);
|
||
return {
|
||
label,
|
||
relative: formatDistanceToNow(date, { addSuffix: true }),
|
||
absolute: formatDate(date, { includeTime: true })
|
||
};
|
||
} catch (error) {
|
||
return { label, relative: dateString, absolute: dateString };
|
||
}
|
||
};
|
||
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<div
|
||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
|
||
onClick={(e) => e.target === e.currentTarget && onClose()}
|
||
>
|
||
<div className="bg-white rounded-lg w-full max-w-4xl mx-4 max-h-[90vh] flex flex-col">
|
||
{/* Header */}
|
||
<div className="p-6 border-b">
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<h3 className="text-xl font-semibold text-gray-900">
|
||
{task?.task_name}
|
||
</h3>
|
||
<Badge variant={getPriorityVariant(task?.priority)} size="sm">
|
||
{t(`tasks.${task?.priority}`)}
|
||
</Badge>
|
||
<Badge variant={getStatusVariant(task?.status)} size="sm">
|
||
{t(`taskStatus.${task?.status}`)}
|
||
</Badge>
|
||
</div>
|
||
<p className="text-sm text-gray-600 mb-3">
|
||
{t("tasks.project")}: <span className="font-medium">{task?.project_name}</span>
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-gray-400 hover:text-gray-600 text-xl font-semibold ml-4"
|
||
onKeyDown={handleKeyDown}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{/* Task Details Grid */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg">
|
||
{/* Location Information */}
|
||
{(task?.city || task?.address) && (
|
||
<div className="space-y-1">
|
||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("projects.locationDetails")}</h4>
|
||
{task?.city && (
|
||
<p className="text-sm text-gray-900">{task.city}</p>
|
||
)}
|
||
{task?.address && (
|
||
<p className="text-xs text-gray-600">{task.address}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Assignment Information */}
|
||
<div className="space-y-1">
|
||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("tasks.assignedTo")}</h4>
|
||
{task?.assigned_to_name ? (
|
||
<div>
|
||
<p className="text-sm text-gray-900">{task.assigned_to_name}</p>
|
||
<p className="text-xs text-gray-600">{task.assigned_to_email}</p>
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-gray-500 italic">{t("projects.unassigned")}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Task Timing */}
|
||
<div className="space-y-1">
|
||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("tasks.dateCreated")}</h4>
|
||
{task?.max_wait_days && (
|
||
<p className="text-xs text-gray-600">{t("tasks.maxWait")}: {task.max_wait_days} {t("tasks.days")}</p>
|
||
)}
|
||
{(() => {
|
||
if (task?.status === "completed" && task?.date_completed) {
|
||
const dateInfo = formatTaskDate(task.date_completed, t("taskStatus.completed"));
|
||
return (
|
||
<div>
|
||
<p className="text-sm text-green-700 font-medium">{dateInfo.relative}</p>
|
||
<p className="text-xs text-gray-600">{dateInfo.absolute}</p>
|
||
</div>
|
||
);
|
||
} else if (task?.status === "in_progress" && task?.date_started) {
|
||
const dateInfo = formatTaskDate(task.date_started, t("tasks.dateStarted"));
|
||
return (
|
||
<div>
|
||
<p className="text-sm text-blue-700 font-medium">{t("tasks.dateStarted")} {dateInfo.relative}</p>
|
||
<p className="text-xs text-gray-600">{dateInfo.absolute}</p>
|
||
</div>
|
||
);
|
||
} else if (task?.date_added) {
|
||
const dateInfo = formatTaskDate(task.date_added, t("tasks.dateCreated"));
|
||
return (
|
||
<div>
|
||
<p className="text-sm text-gray-700 font-medium">{t("tasks.dateCreated")} {dateInfo.relative}</p>
|
||
<p className="text-xs text-gray-600">{dateInfo.absolute}</p>
|
||
</div>
|
||
);
|
||
}
|
||
return null;
|
||
})()}
|
||
</div>
|
||
|
||
{/* Task Description */}
|
||
{task?.description && (
|
||
<div className="space-y-1 md:col-span-2 lg:col-span-3">
|
||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{t("tasks.description")}</h4>
|
||
<p className="text-sm text-gray-900 leading-relaxed">{task.description}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
{loading ? (
|
||
<div className="text-center py-8">
|
||
<div className="animate-pulse space-y-4">
|
||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||
<div className="h-4 bg-gray-200 rounded w-2/3"></div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<h5 className="text-lg font-medium text-gray-900">
|
||
{t("tasks.comments")}
|
||
</h5>
|
||
<Badge variant="secondary" size="sm">
|
||
{notes.length}
|
||
</Badge>
|
||
</div>
|
||
|
||
{notes.length === 0 ? (
|
||
<div className="text-center py-12 text-gray-500">
|
||
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
||
<span className="text-2xl">💬</span>
|
||
</div>
|
||
<p className="text-lg font-medium mb-1">{t("tasks.noComments")}</p>
|
||
<p className="text-sm">Bądź pierwszy, który doda komentarz!</p>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{notes.map((note) => (
|
||
<div
|
||
key={note.note_id}
|
||
className={`p-4 rounded-lg border flex justify-between items-start transition-colors ${
|
||
note.is_system
|
||
? "bg-blue-50 border-blue-200 hover:bg-blue-100"
|
||
: "bg-white border-gray-200 hover:bg-gray-50 shadow-sm"
|
||
}`}
|
||
>
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
{note.is_system ? (
|
||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full font-medium flex items-center gap-1">
|
||
<span>🤖</span>
|
||
{t("admin.system")}
|
||
</span>
|
||
) : (
|
||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded-full font-medium flex items-center gap-1">
|
||
<span>👤</span>
|
||
{note.created_by_name || t("userRoles.user")}
|
||
</span>
|
||
)}
|
||
<span className="text-xs text-gray-500 font-medium">
|
||
{formatDate(note.note_date, {
|
||
includeTime: true,
|
||
})}
|
||
</span>
|
||
</div>
|
||
<p className="text-sm text-gray-800 leading-relaxed">
|
||
{note.note}
|
||
</p>
|
||
</div>
|
||
{!note.is_system && (
|
||
<button
|
||
onClick={() => handleDeleteNote(note.note_id)}
|
||
className="ml-3 p-1 text-red-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
||
title={t("common.delete")}
|
||
>
|
||
<span className="text-sm">🗑️</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer - Add new comment */}
|
||
<div className="p-6 border-t bg-gradient-to-r from-gray-50 to-gray-100">
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-lg">💬</span>
|
||
<label className="text-sm font-medium text-gray-700">
|
||
{t("tasks.addComment")}
|
||
</label>
|
||
</div>
|
||
<textarea
|
||
value={newNote}
|
||
onChange={(e) => setNewNote(e.target.value)}
|
||
placeholder={t("common.addNotePlaceholder")}
|
||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none shadow-sm"
|
||
rows={3}
|
||
onKeyDown={handleKeyDown}
|
||
/>
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||
<span>⌨️</span>
|
||
Naciśnij Ctrl+Enter aby wysłać lub Escape aby zamknąć
|
||
</p>
|
||
<div className="flex gap-3">
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={onClose}
|
||
>
|
||
{t("common.close")}
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
onClick={handleAddNote}
|
||
disabled={loadingAdd || !newNote.trim()}
|
||
>
|
||
{loadingAdd ? t("common.saving") : `💾 ${t("tasks.addComment")}`}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|