Files
panel/src/components/TaskCommentsModal.js

372 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}