- Remove ref-mirror useEffect in AuthContext (cachedUserRef already written at mutation sites) - Replace useEffect slide direction in ReceivedInvoices with render-time computation - Fix Login.tsx useEffect dependency array (mount-only alert should have [] deps) - Move "project created" alert to navigation source in ProjectCreate, remove useEffect in ProjectDetail - Move companySettings defaults into fetch callbacks in InvoiceDetail and OfferDetail - Replace due_date useEffect with useMemo in InvoiceDetail - Capture initial snapshots from API data instead of useEffect in InvoiceDetail, OfferDetail, OrderDetail - Replace localStorage draft useEffect with lazy useState initializer in OfferDetail - Fix attendance dropdown to filter by attendance.record permission only - Fix clock-out 404 on update-address (remove departure_time filter for departure action) - Fix received-invoices stats endpoint referencing non-existent is_deleted and amount_czk columns Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
774 lines
24 KiB
TypeScript
774 lines
24 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
||
import { useAlert } from "../context/AlertContext";
|
||
import { useAuth } from "../context/AuthContext";
|
||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||
import { motion } from "framer-motion";
|
||
|
||
import Forbidden from "../components/Forbidden";
|
||
import ConfirmModal from "../components/ConfirmModal";
|
||
import FormField from "../components/FormField";
|
||
import AdminDatePicker from "../components/AdminDatePicker";
|
||
import ProjectFileManager from "../components/ProjectFileManager";
|
||
import apiFetch from "../utils/api";
|
||
|
||
const API_BASE = "/api/admin";
|
||
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
aktivni: "Aktivní",
|
||
dokonceny: "Dokončený",
|
||
zruseny: "Zrušený",
|
||
};
|
||
|
||
function formatNoteDate(dateStr: string) {
|
||
if (!dateStr) return "";
|
||
const d = new Date(dateStr);
|
||
const day = d.getDate();
|
||
const month = d.getMonth() + 1;
|
||
const year = d.getFullYear();
|
||
const hours = String(d.getHours()).padStart(2, "0");
|
||
const mins = String(d.getMinutes()).padStart(2, "0");
|
||
return `${day}. ${month}. ${year} ${hours}:${mins}`;
|
||
}
|
||
|
||
interface Note {
|
||
id: number;
|
||
content: string;
|
||
user_name: string;
|
||
created_at: string;
|
||
}
|
||
|
||
interface User {
|
||
id: number;
|
||
name: string;
|
||
}
|
||
|
||
interface ProjectData {
|
||
id: number;
|
||
project_number: string;
|
||
name: string;
|
||
status: string;
|
||
start_date: string;
|
||
end_date: string;
|
||
customer_name: string;
|
||
responsible_user_id: string;
|
||
notes?: string;
|
||
order_id?: number;
|
||
order_number?: string;
|
||
order_status?: string;
|
||
quotation_id?: number;
|
||
quotation_number?: string;
|
||
has_nas_folder?: boolean;
|
||
}
|
||
|
||
interface ProjectForm {
|
||
name: string;
|
||
status: string;
|
||
start_date: string;
|
||
end_date: string;
|
||
responsible_user_id: string;
|
||
}
|
||
|
||
export default function ProjectDetail() {
|
||
const { id } = useParams();
|
||
const alert = useAlert();
|
||
const { hasPermission, isAdmin } = useAuth();
|
||
const navigate = useNavigate();
|
||
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [project, setProject] = useState<ProjectData | null>(null);
|
||
const [form, setForm] = useState<ProjectForm>({
|
||
name: "",
|
||
status: "aktivni",
|
||
start_date: "",
|
||
end_date: "",
|
||
responsible_user_id: "",
|
||
});
|
||
const [users, setUsers] = useState<User[]>([]);
|
||
|
||
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
||
const [deleting, setDeleting] = useState(false);
|
||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||
|
||
// Dynamic notes
|
||
const [notes, setNotes] = useState<Note[]>([]);
|
||
const [notesLoading, setNotesLoading] = useState(true);
|
||
const [newNote, setNewNote] = useState("");
|
||
const [addingNote, setAddingNote] = useState(false);
|
||
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
||
|
||
const fetchNotes = async () => {
|
||
try {
|
||
const response = await apiFetch(`${API_BASE}/projects/${id}`);
|
||
if (response.status === 401) return;
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
setNotes(result.data.project_notes || []);
|
||
}
|
||
} catch {
|
||
// silent - notes are supplementary
|
||
} finally {
|
||
setNotesLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
const fetchDetail = async () => {
|
||
try {
|
||
const response = await apiFetch(`${API_BASE}/projects/${id}`);
|
||
if (response.status === 401) return;
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
const p = result.data;
|
||
setProject(p);
|
||
setForm({
|
||
name: p.name || "",
|
||
status: p.status || "aktivni",
|
||
start_date: (p.start_date || "").substring(0, 10),
|
||
end_date: (p.end_date || "").substring(0, 10),
|
||
responsible_user_id: p.responsible_user_id || "",
|
||
});
|
||
} else {
|
||
alert.error(result.error || "Nepodařilo se načíst projekt");
|
||
navigate("/projects");
|
||
}
|
||
} catch {
|
||
alert.error("Chyba připojení");
|
||
navigate("/projects");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
const fetchUsers = async () => {
|
||
try {
|
||
const res = await apiFetch(`${API_BASE}/users`);
|
||
if (res.status === 401) return;
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
const raw = Array.isArray(data.data)
|
||
? data.data
|
||
: data.data?.items || [];
|
||
setUsers(
|
||
raw.map((u: any) => ({
|
||
id: u.id,
|
||
name:
|
||
`${u.first_name || ""} ${u.last_name || ""}`.trim() ||
|
||
u.username,
|
||
})),
|
||
);
|
||
}
|
||
} catch {
|
||
// silent
|
||
}
|
||
};
|
||
|
||
fetchDetail();
|
||
fetchNotes();
|
||
fetchUsers();
|
||
}, [id, alert, navigate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
||
if (!hasPermission("projects.view")) return <Forbidden />;
|
||
|
||
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 {
|
||
const response = await apiFetch(`${API_BASE}/projects/${id}`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
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,
|
||
}),
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
alert.success(result.message || "Projekt byl aktualizován");
|
||
} else {
|
||
alert.error(result.error || "Nepodařilo se uložit projekt");
|
||
}
|
||
} catch {
|
||
alert.error("Chyba připojení");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async () => {
|
||
setDeleting(true);
|
||
try {
|
||
const response = await apiFetch(`${API_BASE}/projects/${id}`, {
|
||
method: "DELETE",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ delete_files: deleteFiles }),
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
navigate("/projects");
|
||
setTimeout(() => alert.success("Projekt byl smazán"), 300);
|
||
} else {
|
||
alert.error(result.error || "Nepodařilo se smazat projekt");
|
||
}
|
||
} catch {
|
||
alert.error("Chyba připojení");
|
||
} finally {
|
||
setDeleting(false);
|
||
}
|
||
};
|
||
|
||
const handleAddNote = async () => {
|
||
if (!newNote.trim()) return;
|
||
|
||
setAddingNote(true);
|
||
try {
|
||
const response = await apiFetch(`${API_BASE}/projects/${id}/notes`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ content: newNote.trim() }),
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
setNotes((prev) => [result.data.note, ...prev]);
|
||
setNewNote("");
|
||
alert.success("Poznámka byla přidána");
|
||
} else {
|
||
alert.error(result.error || "Nepodařilo se přidat poznámku");
|
||
}
|
||
} catch {
|
||
alert.error("Chyba připojení");
|
||
} finally {
|
||
setAddingNote(false);
|
||
}
|
||
};
|
||
|
||
const handleDeleteNote = async (noteId: number) => {
|
||
setDeletingNoteId(noteId);
|
||
try {
|
||
const response = await apiFetch(
|
||
`${API_BASE}/projects/${id}/notes/${noteId}`,
|
||
{
|
||
method: "DELETE",
|
||
},
|
||
);
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
setNotes((prev) => prev.filter((n) => n.id !== noteId));
|
||
alert.success("Poznámka byla smazána");
|
||
} else {
|
||
alert.error(result.error || "Nepodařilo se smazat poznámku");
|
||
}
|
||
} catch {
|
||
alert.error("Chyba připojení");
|
||
} finally {
|
||
setDeletingNoteId(null);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
||
<div
|
||
className="admin-skeleton-row"
|
||
style={{ justifyContent: "space-between" }}
|
||
>
|
||
<div className="flex-row-gap">
|
||
<div
|
||
className="admin-skeleton-line"
|
||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
||
/>
|
||
<div
|
||
className="admin-skeleton-line h-8"
|
||
style={{ width: "200px" }}
|
||
/>
|
||
</div>
|
||
<div className="admin-skeleton-row" style={{ gap: "0.5rem" }}>
|
||
<div
|
||
className="admin-skeleton-line h-10"
|
||
style={{ width: "100px", borderRadius: "8px" }}
|
||
/>
|
||
<div
|
||
className="admin-skeleton-line h-10"
|
||
style={{ width: "100px", borderRadius: "8px" }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="admin-card">
|
||
<div className="admin-skeleton" style={{ gap: "1.25rem" }}>
|
||
{[0, 1, 2, 3].map((i) => (
|
||
<div key={i} className="admin-skeleton-row">
|
||
<div className="admin-skeleton-line w-1/4" />
|
||
<div className="admin-skeleton-line w-1/2" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!project) return null;
|
||
|
||
const canEdit = hasPermission("projects.edit");
|
||
|
||
return (
|
||
<div>
|
||
{/* Header */}
|
||
<motion.div
|
||
className="admin-page-header"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.25 }}
|
||
>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||
<Link
|
||
to="/projects"
|
||
className="admin-btn-icon"
|
||
title="Zpět"
|
||
aria-label="Zpět"
|
||
>
|
||
<svg
|
||
width="20"
|
||
height="20"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
>
|
||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||
</svg>
|
||
</Link>
|
||
<div>
|
||
<h1 className="admin-page-title">
|
||
Projekt {project.project_number}
|
||
</h1>
|
||
</div>
|
||
</div>
|
||
{canEdit && (
|
||
<div className="admin-page-actions">
|
||
<button
|
||
onClick={handleSave}
|
||
className="admin-btn admin-btn-primary"
|
||
disabled={saving}
|
||
>
|
||
{saving ? (
|
||
<>
|
||
<div className="admin-spinner admin-spinner-sm" />
|
||
Ukládání...
|
||
</>
|
||
) : (
|
||
"Uložit"
|
||
)}
|
||
</button>
|
||
{!project.order_id && (
|
||
<button
|
||
onClick={() => setDeleteConfirm(true)}
|
||
className="admin-btn admin-btn-primary"
|
||
>
|
||
Smazat
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
|
||
{/* Form */}
|
||
<motion.div
|
||
className="admin-card"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.25, delay: 0.06 }}
|
||
>
|
||
<div className="admin-card-body">
|
||
<h3 className="admin-card-title">Základní údaje</h3>
|
||
<div className="admin-form">
|
||
<div className="admin-form-row">
|
||
<FormField label="Číslo projektu">
|
||
<input
|
||
type="text"
|
||
value={project.project_number}
|
||
className="admin-form-input"
|
||
readOnly
|
||
style={{
|
||
backgroundColor: "var(--bg-secondary)",
|
||
cursor: "default",
|
||
}}
|
||
/>
|
||
</FormField>
|
||
<FormField label="Název">
|
||
<input
|
||
type="text"
|
||
value={form.name}
|
||
onChange={(e) => updateForm("name", e.target.value)}
|
||
className="admin-form-input"
|
||
placeholder="Název projektu"
|
||
disabled={!canEdit}
|
||
/>
|
||
</FormField>
|
||
</div>
|
||
|
||
<div className="admin-form-row">
|
||
<FormField label="Zákazník">
|
||
<input
|
||
type="text"
|
||
value={project.customer_name || "—"}
|
||
className="admin-form-input"
|
||
readOnly
|
||
style={{
|
||
backgroundColor: "var(--bg-secondary)",
|
||
cursor: "default",
|
||
}}
|
||
/>
|
||
</FormField>
|
||
<FormField label="Zodpovědná osoba">
|
||
<select
|
||
value={form.responsible_user_id}
|
||
onChange={(e) =>
|
||
updateForm("responsible_user_id", e.target.value)
|
||
}
|
||
className="admin-form-select"
|
||
disabled={!canEdit}
|
||
>
|
||
<option value="">— Nevybráno —</option>
|
||
{users.map((u) => (
|
||
<option key={u.id} value={u.id}>
|
||
{u.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</FormField>
|
||
</div>
|
||
|
||
<div className="admin-form-row admin-form-row-3">
|
||
<FormField label="Stav">
|
||
<select
|
||
value={form.status}
|
||
onChange={(e) => updateForm("status", e.target.value)}
|
||
className="admin-form-select"
|
||
disabled={!canEdit}
|
||
>
|
||
<option value="aktivni">Aktivní</option>
|
||
<option value="dokonceny">Dokončený</option>
|
||
<option value="zruseny">Zrušený</option>
|
||
</select>
|
||
</FormField>
|
||
<FormField label="Datum zahájení">
|
||
<AdminDatePicker
|
||
mode="date"
|
||
value={form.start_date}
|
||
onChange={(val: string) => updateForm("start_date", val)}
|
||
disabled={!canEdit}
|
||
/>
|
||
</FormField>
|
||
<FormField label="Datum ukončení">
|
||
<AdminDatePicker
|
||
mode="date"
|
||
value={form.end_date}
|
||
onChange={(val: string) => updateForm("end_date", val)}
|
||
disabled={!canEdit}
|
||
/>
|
||
</FormField>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Notes */}
|
||
<motion.div
|
||
className="admin-card"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.25, delay: 0.08 }}
|
||
>
|
||
<div className="admin-card-body">
|
||
<h3 className="admin-card-title">Poznámky</h3>
|
||
|
||
{/* Add note */}
|
||
<div className="mb-4">
|
||
<textarea
|
||
value={newNote}
|
||
onChange={(e) => setNewNote(e.target.value)}
|
||
className="admin-form-input"
|
||
rows={2}
|
||
placeholder="Napište poznámku..."
|
||
style={{ resize: "vertical", width: "100%" }}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter" && e.ctrlKey && newNote.trim()) {
|
||
handleAddNote();
|
||
}
|
||
}}
|
||
/>
|
||
<div className="mt-2">
|
||
<button
|
||
onClick={handleAddNote}
|
||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||
disabled={addingNote || !newNote.trim()}
|
||
>
|
||
{addingNote ? (
|
||
<div className="admin-spinner admin-spinner-sm" />
|
||
) : (
|
||
"Přidat poznámku"
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Legacy notes (read-only) */}
|
||
{project.notes && (
|
||
<div
|
||
style={{
|
||
padding: "0.75rem",
|
||
background: "var(--bg-secondary)",
|
||
borderRadius: "0.5rem",
|
||
marginBottom: "0.5rem",
|
||
fontSize: "0.85rem",
|
||
color: "var(--text-secondary)",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: "0.75rem",
|
||
color: "var(--text-tertiary)",
|
||
marginBottom: "0.25rem",
|
||
}}
|
||
>
|
||
Starší poznámka (před zavedením systému)
|
||
</div>
|
||
<div style={{ whiteSpace: "pre-wrap" }}>{project.notes}</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Notes list */}
|
||
{notesLoading && (
|
||
<div className="admin-skeleton" style={{ gap: "0.75rem" }}>
|
||
{[0, 1, 2].map((i) => (
|
||
<div
|
||
key={i}
|
||
className="admin-skeleton-line"
|
||
style={{ height: "52px", borderRadius: "8px" }}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
{!notesLoading && notes.length === 0 && !project.notes && (
|
||
<div
|
||
style={{
|
||
color: "var(--text-tertiary)",
|
||
fontSize: "0.875rem",
|
||
textAlign: "center",
|
||
padding: "1rem 0",
|
||
}}
|
||
>
|
||
Zatím žádné poznámky
|
||
</div>
|
||
)}
|
||
{!notesLoading && (notes.length > 0 || project.notes) && (
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
gap: "0.5rem",
|
||
}}
|
||
>
|
||
{notes.map((note) => (
|
||
<div
|
||
key={note.id}
|
||
style={{
|
||
padding: "0.75rem",
|
||
background: "var(--bg-secondary)",
|
||
borderRadius: "0.5rem",
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "flex-start",
|
||
gap: "0.5rem",
|
||
}}
|
||
>
|
||
<div className="flex-1">
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "0.5rem",
|
||
marginBottom: "0.25rem",
|
||
}}
|
||
>
|
||
<span style={{ fontWeight: 600, fontSize: "0.85rem" }}>
|
||
{note.user_name}
|
||
</span>
|
||
<span
|
||
style={{
|
||
color: "var(--text-tertiary)",
|
||
fontSize: "0.75rem",
|
||
}}
|
||
>
|
||
{formatNoteDate(note.created_at)}
|
||
</span>
|
||
</div>
|
||
<div
|
||
style={{
|
||
whiteSpace: "pre-wrap",
|
||
fontSize: "0.875rem",
|
||
lineHeight: 1.5,
|
||
}}
|
||
>
|
||
{note.content}
|
||
</div>
|
||
</div>
|
||
{isAdmin && (
|
||
<button
|
||
onClick={() => handleDeleteNote(note.id)}
|
||
className="admin-btn-icon"
|
||
title="Smazat poznámku"
|
||
disabled={deletingNoteId === note.id}
|
||
style={{
|
||
flexShrink: 0,
|
||
opacity: deletingNoteId === note.id ? 0.5 : 1,
|
||
}}
|
||
>
|
||
{deletingNoteId === note.id ? (
|
||
<div
|
||
className="admin-spinner"
|
||
style={{ width: 14, height: 14, borderWidth: 2 }}
|
||
/>
|
||
) : (
|
||
<svg
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
>
|
||
<polyline points="3 6 5 6 21 6" />
|
||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||
<path d="M10 11v6M14 11v6" />
|
||
</svg>
|
||
)}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* Project File Manager */}
|
||
<motion.div
|
||
style={{ marginBottom: "1rem" }}
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.25, delay: 0.12 }}
|
||
>
|
||
<ProjectFileManager
|
||
projectId={project.id}
|
||
projectNumber={project.project_number}
|
||
hasPermission={hasPermission}
|
||
hasNasFolder={project.has_nas_folder ?? false}
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* Links */}
|
||
<motion.div
|
||
className="admin-card"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.25, delay: 0.15 }}
|
||
>
|
||
<div className="admin-card-body">
|
||
<h3 className="admin-card-title">Propojení</h3>
|
||
<div className="admin-form-row">
|
||
<FormField label="Objednávka">
|
||
<div>
|
||
{project.order_id ? (
|
||
<Link
|
||
to={`/orders/${project.order_id}`}
|
||
className="link-accent"
|
||
>
|
||
{project.order_number}
|
||
{project.order_status && (
|
||
<span
|
||
className="text-tertiary"
|
||
style={{ fontWeight: 400, marginLeft: "0.5rem" }}
|
||
>
|
||
(
|
||
{STATUS_LABELS[project.order_status] ||
|
||
project.order_status}
|
||
)
|
||
</span>
|
||
)}
|
||
</Link>
|
||
) : (
|
||
"—"
|
||
)}
|
||
</div>
|
||
</FormField>
|
||
<FormField label="Nabídka">
|
||
<div>
|
||
{project.quotation_id ? (
|
||
<Link
|
||
to={`/offers/${project.quotation_id}`}
|
||
className="link-accent"
|
||
>
|
||
{project.quotation_number}
|
||
</Link>
|
||
) : (
|
||
"—"
|
||
)}
|
||
</div>
|
||
</FormField>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<ConfirmModal
|
||
isOpen={deleteConfirm}
|
||
onClose={() => {
|
||
setDeleteConfirm(false);
|
||
setDeleteFiles(false);
|
||
}}
|
||
onConfirm={handleDelete}
|
||
title="Smazat projekt"
|
||
message={
|
||
<>
|
||
Opravdu chcete smazat projekt "{project.project_number} –{" "}
|
||
{project.name}"? Tato akce je nevratná.
|
||
{project.has_nas_folder && (
|
||
<label
|
||
className="admin-form-checkbox"
|
||
style={{ marginTop: "1rem", display: "flex" }}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={deleteFiles}
|
||
onChange={(e) => setDeleteFiles(e.target.checked)}
|
||
/>
|
||
<span>Smazat i soubory na disku</span>
|
||
</label>
|
||
)}
|
||
</>
|
||
}
|
||
confirmText="Smazat"
|
||
cancelText="Zrušit"
|
||
type="danger"
|
||
loading={deleting}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|