import { RequestWorkspace } from "./admin/features/requests/RequestWorkspace.jsx"; import { createRequestModalState } from "./admin/shared/state.js"; import { detectAttachmentPreviewKind, fmtShortDateTime, statusLabel } from "./admin/shared/utils.js"; (function () { const { useCallback, useEffect, useMemo, useRef, useState } = React; function sortRowsByCreatedAt(rows) { return [...rows].sort((left, right) => { const leftTs = new Date(left?.created_at || left?.updated_at || 0).getTime(); const rightTs = new Date(right?.created_at || right?.updated_at || 0).getTime(); if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) return leftTs - rightTs; return String(left?.id || "").localeCompare(String(right?.id || ""), "ru"); }); } function mergeRowsById(existingRows, incomingRows) { const merged = new Map(); (Array.isArray(existingRows) ? existingRows : []).forEach((row) => { const key = String(row?.id || "").trim(); if (key) merged.set(key, row); }); (Array.isArray(incomingRows) ? incomingRows : []).forEach((row) => { const key = String(row?.id || "").trim(); if (key) merged.set(key, row); }); return sortRowsByCreatedAt(Array.from(merged.values())); } function getOldestMessageCursor(rows) { const sorted = sortRowsByCreatedAt(Array.isArray(rows) ? rows : []); const first = sorted[0]; if (!first) return null; const beforeId = String(first.id || "").trim(); const beforeCreatedAt = String(first.created_at || first.updated_at || "").trim(); if (!beforeId || !beforeCreatedAt) return null; return { beforeId, beforeCreatedAt }; } function collectDeferredMessageIds(rows) { return (Array.isArray(rows) ? rows : []) .filter((row) => row && typeof row === "object" && row.body_loaded === false && String(row.id || "").trim()) .map((row) => String(row.id).trim()); } function StatusLine({ status }) { return

{status?.message || ""}

; } function Overlay({ open, id, onClose, children }) { return (
{children}
); } function GlobalTooltipLayer() { const [tooltip, setTooltip] = useState({ open: false, text: "", x: 0, y: 0, maxWidth: 320 }); const activeRef = useRef(null); useEffect(() => { const getTarget = (node) => { if (!(node instanceof Element)) return null; const el = node.closest("[data-tooltip]"); if (!el) return null; const text = String(el.getAttribute("data-tooltip") || "").trim(); return text ? el : null; }; const reposition = (el) => { if (!(el instanceof Element)) return; const text = String(el.getAttribute("data-tooltip") || "").trim(); if (!text) return; const rect = el.getBoundingClientRect(); const vw = window.innerWidth || 0; const maxWidth = Math.min(360, Math.max(140, vw - 24)); const approxWidth = Math.min(maxWidth, Math.max(80, text.length * 7.1 + 22)); const centerX = rect.left + rect.width / 2; const x = Math.max(12 + approxWidth / 2, Math.min(vw - 12 - approxWidth / 2, centerX)); const y = Math.max(8, rect.top - 8); setTooltip({ open: true, text, x, y, maxWidth }); }; const open = (node) => { const target = getTarget(node); if (!target) return; activeRef.current = target; reposition(target); }; const closeIfNeeded = (related) => { const current = activeRef.current; if (!current) return; if (related instanceof Element) { if (related === current || current.contains(related)) return; const nextTarget = getTarget(related); if (nextTarget === current) return; } activeRef.current = null; setTooltip((prev) => ({ ...prev, open: false })); }; const onMouseOver = (event) => open(event.target); const onFocusIn = (event) => open(event.target); const onMouseOut = (event) => closeIfNeeded(event.relatedTarget); const onFocusOut = (event) => closeIfNeeded(event.relatedTarget); const onUpdatePosition = () => { if (activeRef.current) reposition(activeRef.current); }; document.addEventListener("mouseover", onMouseOver, true); document.addEventListener("focusin", onFocusIn, true); document.addEventListener("mouseout", onMouseOut, true); document.addEventListener("focusout", onFocusOut, true); window.addEventListener("scroll", onUpdatePosition, true); window.addEventListener("resize", onUpdatePosition); return () => { document.removeEventListener("mouseover", onMouseOver, true); document.removeEventListener("focusin", onFocusIn, true); document.removeEventListener("mouseout", onMouseOut, true); document.removeEventListener("focusout", onFocusOut, true); window.removeEventListener("scroll", onUpdatePosition, true); window.removeEventListener("resize", onUpdatePosition); }; }, []); return (
{tooltip.text}
); } function AttachmentPreviewModal({ open, title, url, fileName, mimeType, onClose }) { const [resolvedUrl, setResolvedUrl] = useState(""); const [resolvedText, setResolvedText] = useState(""); const [resolvedKind, setResolvedKind] = useState(""); const [hint, setHint] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const decodeTextPreview = (arrayBuffer) => { const bytes = new Uint8Array(arrayBuffer || new ArrayBuffer(0)); const sampleLength = Math.min(bytes.length, 4096); let suspicious = 0; for (let i = 0; i < sampleLength; i += 1) { const byte = bytes[i]; if (byte === 0) suspicious += 4; else if (byte < 9 || (byte > 13 && byte < 32)) suspicious += 1; } if (sampleLength && suspicious / sampleLength > 0.08) return null; const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes).replace(/\u0000/g, ""); return text.length > 200000 ? text.slice(0, 200000) + "\n\n[Текст обрезан для предпросмотра]" : text; }; useEffect(() => { if (!open || !url) { setResolvedUrl(""); setResolvedText(""); setResolvedKind(""); setHint(""); setLoading(false); setError(""); return; } const kind = detectAttachmentPreviewKind(fileName, mimeType); setResolvedKind(kind); setResolvedText(""); setHint(""); if (kind === "none") { setResolvedUrl(""); setLoading(false); setError(""); return; } let cancelled = false; let objectUrl = ""; setLoading(true); setError(""); setResolvedUrl(""); (async () => { try { const response = await fetch(url, { credentials: "same-origin" }); if (!response.ok) throw new Error("Не удалось загрузить файл для предпросмотра"); const buffer = await response.arrayBuffer(); if (cancelled) return; if (kind === "pdf") { const header = new Uint8Array(buffer.slice(0, 5)); const isPdf = header.length >= 5 && header[0] === 0x25 && header[1] === 0x50 && header[2] === 0x44 && header[3] === 0x46 && header[4] === 0x2d; if (isPdf) { setResolvedUrl(String(url)); setResolvedKind("pdf"); setLoading(false); return; } const textPreview = decodeTextPreview(buffer); if (textPreview != null) { setResolvedUrl(""); setResolvedText(textPreview); setResolvedKind("text"); setHint("Файл помечен как PDF, но не является валидным PDF. Показан текстовый предпросмотр."); setLoading(false); return; } throw new Error("Файл помечен как PDF, но не является валидным PDF-документом."); } if (kind === "text") { const textPreview = decodeTextPreview(buffer); if (textPreview == null) throw new Error("Не удалось распознать текстовый файл для предпросмотра."); setResolvedUrl(""); setResolvedText(textPreview); setResolvedKind("text"); setLoading(false); return; } const blob = new Blob([buffer], { type: response.headers.get("content-type") || mimeType || "application/octet-stream" }); objectUrl = URL.createObjectURL(blob); if (cancelled) { URL.revokeObjectURL(objectUrl); return; } setResolvedUrl(objectUrl); setResolvedKind(kind); setLoading(false); } catch (err) { if (cancelled) return; setError(err instanceof Error ? err.message : "Не удалось открыть предпросмотр"); setLoading(false); } })(); return () => { cancelled = true; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [fileName, mimeType, open, url]); if (!open || !url) return null; const kind = resolvedKind || detectAttachmentPreviewKind(fileName, mimeType); return ( event.target.id === "file-preview-overlay" && onClose()}>
event.stopPropagation()}>

{title || fileName || "Предпросмотр файла"}

{loading ?

Загрузка предпросмотра...

: null} {!loading && !error && hint ?

{hint}

: null} {error ?

{error}

: null} {!loading && !error && kind === "image" && resolvedUrl ? ( {fileName ) : null} {!loading && !error && kind === "video" && resolvedUrl ? (