import { useState, useEffect, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useApiMutation } from "../lib/queries/mutations";
import apiFetch from "../utils/api";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
import { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import MenuItem from "@mui/material/MenuItem";
import {
projectDetailOptions,
type ProjectNote,
} from "../lib/queries/projects";
import { userListOptions, type User as ApiUser } from "../lib/queries/users";
import Forbidden from "../components/Forbidden";
import ProjectFileManager from "../components/ProjectFileManager";
import NoteCard from "../components/NoteCard";
import {
Button,
Card,
Field,
TextField,
Select,
DateField,
StatusChip,
CheckboxField,
ConfirmDialog,
LoadingState,
PageEnter,
headerActionsSx,
} from "../ui";
import {
PROJECT_STATUS,
ORDER_STATUS,
statusLabel,
statusColor,
} from "../lib/documentStatus";
const API_BASE = "/api/admin";
interface User {
id: number;
name: string;
}
interface ProjectForm {
name: string;
status: string;
start_date: string;
end_date: string;
responsible_user_id: string;
}
const BackIcon = (
);
export default function ProjectDetail() {
const { id } = useParams();
const alert = useAlert();
const { hasPermission, isAdmin, user } = useAuth();
const navigate = useNavigate();
const qc = useQueryClient();
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({
name: "",
status: "aktivni",
start_date: "",
end_date: "",
responsible_user_id: "",
});
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [deleting, setDeleting] = useState(false);
const [deleteFiles, setDeleteFiles] = useState(false);
// Dynamic notes
const [newNote, setNewNote] = useState("");
const [addingNote, setAddingNote] = useState(false);
const [deletingNoteId, setDeletingNoteId] = useState(null);
const projectQuery = useQuery(projectDetailOptions(id));
const project = projectQuery.data;
const isPending = projectQuery.isPending;
const notes: ProjectNote[] = project?.project_notes || [];
const { data: usersData } = useQuery(userListOptions("projects.view"));
// Only active users with projects.view — but keep the project's CURRENT
// assignee in the list even if they're now inactive, so the select still
// reflects the saved value (marked "(neaktivní)" so it's clear).
const assignedUserId = project?.responsible_user_id || "";
const users: User[] = (usersData ?? [])
.filter((u: ApiUser) => u.is_active || String(u.id) === assignedUserId)
.map((u: ApiUser) => ({
id: u.id,
name:
(`${u.first_name || ""} ${u.last_name || ""}`.trim() || u.username) +
(u.is_active ? "" : " (neaktivní)"),
}));
// Reset form sync when navigating to a different project
const formInitialized = useRef(false);
useEffect(() => {
formInitialized.current = false;
}, [id]);
// Sync project data to local form state on first load
useEffect(() => {
if (project && !formInitialized.current) {
setForm({
name: project.name || "",
status: project.status || "aktivni",
start_date: (project.start_date || "").substring(0, 10),
end_date: (project.end_date || "").substring(0, 10),
responsible_user_id: project.responsible_user_id || "",
});
formInitialized.current = true;
}
}, [project]);
// Navigate away on fetch error
useEffect(() => {
if (projectQuery.error) {
alert.error("Nepodařilo se načíst projekt");
navigate("/projects");
}
}, [projectQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps
const projectSaveMutation = useApiMutation<
{
name: string;
status: string;
start_date: string | null;
end_date: string | null;
responsible_user_id: string | null;
},
unknown
>({
url: () => `${API_BASE}/projects/${id}`,
method: () => "PUT",
invalidate: ["projects", "warehouse"],
});
const projectDeleteMutation = useApiMutation<
{ delete_files: boolean },
unknown
>({
url: () => `${API_BASE}/projects/${id}`,
method: () => "DELETE",
invalidate: ["projects", "warehouse"],
});
const projectAddNoteMutation = useApiMutation<{ content: string }, unknown>({
url: () => `${API_BASE}/projects/${id}/notes`,
method: () => "POST",
invalidate: ["projects", "warehouse"],
});
const projectDeleteNoteMutation = useApiMutation({
url: (noteId) => `${API_BASE}/projects/${id}/notes/${noteId}`,
method: () => "DELETE",
invalidate: ["projects", "warehouse"],
});
if (!hasPermission("projects.view")) return ;
const updateForm = (field: keyof ProjectForm, value: string) =>
setForm((prev) => ({ ...prev, [field]: value }));
const handleSave = async () => {
if (!form.name.trim()) {
alert.error("Název projektu je povinný");
return;
}
setSaving(true);
try {
await projectSaveMutation.mutateAsync({
name: form.name,
status: form.status,
start_date: form.start_date || null,
end_date: form.end_date || null,
responsible_user_id: form.responsible_user_id || null,
});
alert.success("Projekt byl aktualizován");
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
setDeleting(true);
try {
await projectDeleteMutation.mutateAsync({ delete_files: deleteFiles });
navigate("/projects");
setTimeout(() => alert.success("Projekt byl smazán"), 300);
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
} finally {
setDeleting(false);
}
};
const handleAddNote = async () => {
if (!newNote.trim()) return;
setAddingNote(true);
try {
await projectAddNoteMutation.mutateAsync({ content: newNote.trim() });
setNewNote("");
alert.success("Poznámka byla přidána");
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
} finally {
setAddingNote(false);
}
};
const handleDeleteNote = async (noteId: number) => {
setDeletingNoteId(noteId);
try {
await projectDeleteNoteMutation.mutateAsync(noteId);
alert.success("Poznámka byla smazána");
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
} finally {
setDeletingNoteId(null);
}
};
// Throws on failure so NoteCard keeps its editor open with the draft.
const saveNoteEdit = async (noteId: number, content: string) => {
// Raw apiFetch: the body must carry ONLY content (strict schema), the
// note id lives in the URL — useApiMutation sends its whole input.
let res: Response;
try {
res = await apiFetch(`${API_BASE}/projects/${id}/notes/${noteId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
} catch {
alert.error("Chyba připojení");
throw new Error("network");
}
const body = await res.json().catch(() => null);
if (!res.ok) {
alert.error(body?.error || "Uložení selhalo");
throw new Error(body?.error || "Uložení selhalo");
}
qc.invalidateQueries({ queryKey: ["projects"] });
alert.success("Poznámka upravena");
};
if (isPending) {
return ;
}
if (!project) return null;
const canEdit = hasPermission("projects.edit");
return (
{/* Header */}
{/* flexWrap: the title must drop below the Zpět button on phones
instead of being squeezed beside it (mirrors Offer/Invoice
detail headers). */}
Projekt
{project.project_number}
{canEdit && (
{!project.order_id && (
)}
)}
{/* Basic info form */}
Základní údaje
updateForm("name", e.target.value)}
placeholder="Název projektu"
fullWidth
disabled={!canEdit}
/>
updateForm("start_date", val)}
disabled={!canEdit}
/>
updateForm("end_date", val)}
disabled={!canEdit}
/>
{/* Notes */}
Poznámky
{/* Add note */}
setNewNote(e.target.value)}
multiline
minRows={2}
placeholder="Napište poznámku..."
fullWidth
onKeyDown={(e) => {
if (e.key === "Enter" && e.ctrlKey && newNote.trim()) {
handleAddNote();
}
}}
/>
{/* Legacy notes (read-only) */}
{project.notes && (
Starší poznámka (před zavedením systému)
{project.notes}
)}
{/* Notes list */}
{notes.length === 0 && !project.notes && (
Zatím žádné poznámky
)}
{(notes.length > 0 || project.notes) && (
{notes.map((note) => (
saveNoteEdit(note.id, content)}
onDelete={() => handleDeleteNote(note.id)}
/>
))}
)}
{/* Project File Manager */}
{/* Links */}
Propojení
Objednávka
{project.order_id ? (
<>
{project.order_number}
{project.order_status && (
)}
>
) : (
"—"
)}
Nabídka
{project.quotation_id ? (
{project.quotation_number}
) : (
"—"
)}
{
setDeleteConfirm(false);
setDeleteFiles(false);
}}
onConfirm={handleDelete}
title="Smazat projekt"
message={`Opravdu chcete smazat projekt „${project.project_number} – ${project.name}"? Tato akce je nevratná.`}
confirmText="Smazat"
confirmVariant="danger"
loading={deleting}
>
{project.has_nas_folder && (
)}
);
}