import { useState, useEffect, useRef, useCallback } from "react"; import { useAlert } from "../context/AlertContext"; import ConfirmModal from "./ConfirmModal"; import apiFetch from "../utils/api"; const API_BASE = "/api/admin"; interface ProjectFileManagerProps { projectId: number; projectNumber: string | null; hasPermission: (perm: string) => boolean; hasNasFolder: boolean; } interface FileItem { name: string; type: "file" | "folder"; size?: number; size_formatted?: string; modified?: string; extension?: string; item_count?: number; is_symlink?: boolean; link_target?: string; } function getFileIcon(type: string, extension?: string) { if (type === "folder") { return ( ); } const ext = (extension || "").toLowerCase(); const iconMap: Record = { pdf: { color: "#e74c3c", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, doc: { color: "#3498db", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, docx: { color: "#3498db", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, xls: { color: "#27ae60", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, xlsx: { color: "#27ae60", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, ppt: { color: "#e67e22", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, pptx: { color: "#e67e22", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, jpg: { color: "#3498db", path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z", }, jpeg: { color: "#3498db", path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z", }, png: { color: "#3498db", path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z", }, gif: { color: "#3498db", path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z", }, zip: { color: "#e67e22", path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z", }, rar: { color: "#e67e22", path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z", }, "7z": { color: "#e67e22", path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z", }, dwg: { color: "#8e44ad", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, dxf: { color: "#8e44ad", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, step: { color: "#8e44ad", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, stp: { color: "#8e44ad", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }, }; const cfg = iconMap[ext] || { color: "var(--text-muted)", path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", }; return ( ); } function SymlinkBadge({ target }: { target?: string }) { return ( ); } function FileNameCell({ item, onFolderClick, }: { item: FileItem; onFolderClick: (name: string) => void; }) { if (item.type === "folder") { return ( {item.is_symlink && } {item.item_count !== undefined && ( {item.item_count} )} ); } return ( {item.name} {item.is_symlink && } ); } export default function ProjectFileManager({ projectId, projectNumber, hasPermission, hasNasFolder, }: ProjectFileManagerProps) { const alert = useAlert(); const fileInputRef = useRef(null); const isCancelling = useRef(false); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [currentPath, setCurrentPath] = useState(""); const [breadcrumb, setBreadcrumb] = useState([""]); const [fullPath, setFullPath] = useState(""); const [dragOver, setDragOver] = useState(false); const [uploading, setUploading] = useState(false); const [newFolderMode, setNewFolderMode] = useState(false); const [newFolderName, setNewFolderName] = useState(""); const [creatingFolder, setCreatingFolder] = useState(false); const [renamingItem, setRenamingItem] = useState(null); const [renameValue, setRenameValue] = useState(""); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const canManage = hasPermission("projects.files"); const fetchFiles = useCallback( async (path = "", options: { ignore?: boolean } = {}) => { setLoading(true); setErrorMessage(null); try { const params = new URLSearchParams({ project_id: String(projectId) }); if (path) { params.set("path", path); } const res = await apiFetch(`${API_BASE}/project-files?${params}`); if (options.ignore) return; if (res.status === 401) return; const data = await res.json(); if (data.success) { setItems(data.data.items || []); setBreadcrumb(data.data.breadcrumb || [""]); setCurrentPath(data.data.path || ""); setFullPath(data.data.full_path || ""); } else if (res.status === 404) { setItems([]); setBreadcrumb([""]); } else { setErrorMessage(data.error || "Nepodařilo se načíst soubory"); } } catch { if (!options.ignore) { setErrorMessage("Chyba připojení"); } } finally { if (!options.ignore) { setLoading(false); } } }, [projectId], ); useEffect(() => { const opts = { ignore: false }; fetchFiles("", opts); return () => { opts.ignore = true; }; }, [fetchFiles]); const navigateTo = (path: string) => { setNewFolderMode(false); setRenamingItem(null); fetchFiles(path); }; const handleBreadcrumbClick = (index: number) => { if (index === 0) { navigateTo(""); return; } const path = breadcrumb.slice(1, index + 1).join("/"); navigateTo(path); }; const handleFolderClick = (folderName: string) => { const path = currentPath ? `${currentPath}/${folderName}` : folderName; navigateTo(path); }; const handleUpload = async (files: FileList | null) => { if (!files || files.length === 0) return; setUploading(true); let successCount = 0; let errorMsg: string | null = null; for (const file of Array.from(files)) { const formData = new FormData(); formData.append("file", file); const params = new URLSearchParams({ action: "upload", project_id: String(projectId), }); if (currentPath) { params.set("path", currentPath); } try { const res = await apiFetch( `${API_BASE}/project-files/upload?${params}`, { method: "POST", body: formData, }, ); const data = await res.json(); if (data.success) { successCount++; } else { errorMsg = data.error || "Chyba při nahrávání"; } } catch { errorMsg = "Chyba připojení"; } } setUploading(false); if (successCount > 0) { const msg = successCount === 1 ? "Soubor byl nahrán" : `Nahráno ${successCount} souborů`; alert.success(msg); fetchFiles(currentPath); } if (errorMsg) { alert.error(errorMsg); } }; const handleFileInputChange = (e: React.ChangeEvent) => { handleUpload(e.target.files); e.target.value = ""; }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); if (!canManage) return; handleUpload(e.dataTransfer.files); }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); if (canManage) { setDragOver(true); } }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); }; const handleCreateFolder = async () => { const name = newFolderName.trim(); if (!name) return; setCreatingFolder(true); try { const params = new URLSearchParams({ action: "create_folder", project_id: String(projectId), }); const res = await apiFetch(`${API_BASE}/project-files?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: currentPath, folder_name: name }), }); const data = await res.json(); if (data.success) { alert.success("Složka byla vytvořena"); setNewFolderMode(false); setNewFolderName(""); fetchFiles(currentPath); } else { alert.error(data.error || "Nepodařilo se vytvořit složku"); } } catch { alert.error("Chyba připojení"); } finally { setCreatingFolder(false); } }; const handleDownload = async (item: FileItem) => { const filePath = currentPath ? `${currentPath}/${item.name}` : item.name; const params = new URLSearchParams({ action: "download", project_id: String(projectId), path: filePath, }); try { const res = await apiFetch(`${API_BASE}/project-files?${params}`); if (!res.ok) { const err = await res.json().catch(() => null); alert.error(err?.error || "Chyba při stahování"); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = item.name; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch { alert.error("Chyba připojení"); } }; const handleDelete = async () => { if (!deleteTarget) return; setDeleting(true); const filePath = currentPath ? `${currentPath}/${deleteTarget.name}` : deleteTarget.name; try { const params = new URLSearchParams({ project_id: String(projectId), path: filePath, }); const res = await apiFetch(`${API_BASE}/project-files?${params}`, { method: "DELETE", }); const data = await res.json(); if (data.success) { alert.success( deleteTarget.type === "folder" ? "Složka byla smazána" : "Soubor byl smazán", ); fetchFiles(currentPath); } else { alert.error(data.error || "Nepodařilo se smazat"); } } catch { alert.error("Chyba připojení"); } finally { setDeleting(false); setDeleteTarget(null); } }; const handleRename = async (item: FileItem) => { const newName = renameValue.trim(); if (!newName || newName === item.name) { setRenamingItem(null); return; } const fromPath = currentPath ? `${currentPath}/${item.name}` : item.name; const toPath = currentPath ? `${currentPath}/${newName}` : newName; try { const params = new URLSearchParams({ action: "move", project_id: String(projectId), }); const res = await apiFetch(`${API_BASE}/project-files?${params}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from_path: fromPath, to_path: toPath }), }); const data = await res.json(); if (data.success) { alert.success("Přejmenováno"); fetchFiles(currentPath); } else { alert.error(data.error || "Nepodařilo se přejmenovat"); } } catch { alert.error("Chyba připojení"); } finally { setRenamingItem(null); } }; const startRename = (item: FileItem) => { setRenamingItem(item.name); setRenameValue(item.name); }; if (loading && items.length === 0 && !errorMessage) { return (

Soubory

{[0, 1, 2, 3].map((i) => (
))}
); } if (errorMessage) { return (

Soubory

{errorMessage}
); } return (

Soubory

{/* Toolbar */}
{breadcrumb.map((segment, i) => ( {i > 0 && /} ))}
{fullPath && ( {fullPath} )} {canManage && (
)}
{/* New folder input */} {newFolderMode && (
setNewFolderName(e.target.value)} className="admin-form-input" placeholder="Název složky..." autoFocus onKeyDown={(e) => { if (e.key === "Enter") handleCreateFolder(); if (e.key === "Escape") { setNewFolderMode(false); setNewFolderName(""); } }} style={{ fontSize: "12px", padding: "6px 10px" }} />
)} {/* Drop zone + table */}
{dragOver && (
Přetáhněte soubory sem
)} {items.length === 0 && !loading ? (
{hasNasFolder ? "Složka je prázdná" : "Složka projektu zatím neexistuje"} {canManage && !hasNasFolder && ( Nahrání souboru ji automaticky vytvoří )}
) : (
{canManage && ( )} {items.map((item) => ( {canManage && ( )} ))}
Název Velikost Změněno Akce
{getFileIcon(item.type, item.extension)} {renamingItem === item.name ? ( setRenameValue(e.target.value)} className="admin-form-input" style={{ fontSize: "11px", padding: "3px 8px", maxWidth: "300px", }} autoFocus onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleRename(item); } if (e.key === "Escape") { e.preventDefault(); isCancelling.current = true; setRenamingItem(null); setRenameValue(item.name); setTimeout(() => { isCancelling.current = false; }, 0); } }} onBlur={() => { if (isCancelling.current) { return; } handleRename(item); }} /> ) : ( )} {item.type === "file" ? item.size_formatted : "\u2014"} {item.modified || "\u2014"}
{item.type === "file" && ( )}
)}
setDeleteTarget(null)} onConfirm={handleDelete} title={ deleteTarget?.type === "folder" ? "Smazat složku" : "Smazat soubor" } message={`Opravdu chcete smazat "${deleteTarget?.name}"?${deleteTarget?.type === "folder" ? " Složka bude smazána včetně veškerého obsahu." : ""}`} confirmText="Smazat" cancelText="Zrušit" type="danger" loading={deleting} />
); }