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 <p className={"status" + (status?.kind ? " " + status.kind : "")}>{status?.message || ""}</p>;
  }

  function Overlay({ open, id, onClose, children }) {
    return (
      <div className={"overlay" + (open ? " open" : "")} id={id} onClick={onClose}>
        {children}
      </div>
    );
  }

  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 (
      <div
        className={"global-tooltip-layer" + (tooltip.open ? " open" : "")}
        style={{ left: tooltip.x + "px", top: tooltip.y + "px", maxWidth: tooltip.maxWidth + "px" }}
        role="tooltip"
        aria-hidden={tooltip.open ? "false" : "true"}
      >
        {tooltip.text}
      </div>
    );
  }

  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 (
      <Overlay open={open} id="file-preview-overlay" onClose={(event) => event.target.id === "file-preview-overlay" && onClose()}>
        <div className="modal request-preview-modal" onClick={(event) => event.stopPropagation()}>
          <div className="modal-head">
            <h3>{title || fileName || "Предпросмотр файла"}</h3>
            <div className="request-preview-head-actions">
              <a className="icon-btn file-action-btn request-preview-download-icon" href={url} target="_blank" rel="noreferrer" aria-label="Скачать файл" data-tooltip="Скачать">
                <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false">
                  <path
                    d="M12 3a1 1 0 0 1 1 1v8.17l2.58-2.58a1 1 0 1 1 1.42 1.42l-4.3 4.3a1 1 0 0 1-1.4 0l-4.3-4.3a1 1 0 0 1 1.42-1.42L11 12.17V4a1 1 0 0 1 1-1zm-7 14a1 1 0 0 1 1 1v1h12v-1a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"
                    fill="currentColor"
                  />
                </svg>
              </a>
              <button className="close" type="button" id="file-preview-close" onClick={onClose}>
                ×
              </button>
            </div>
          </div>
          <div className="request-preview-body" id="file-preview-body">
            {loading ? <p className="request-preview-note">Загрузка предпросмотра...</p> : null}
            {!loading && !error && hint ? <p className="request-preview-note">{hint}</p> : null}
            {error ? <p className="request-preview-note">{error}</p> : null}
            {!loading && !error && kind === "image" && resolvedUrl ? (
              <img className="request-preview-image" src={resolvedUrl} alt={fileName || "attachment"} />
            ) : null}
            {!loading && !error && kind === "video" && resolvedUrl ? (
              <video className="request-preview-video" src={resolvedUrl} controls preload="metadata" />
            ) : null}
            {!loading && !error && kind === "pdf" && resolvedUrl ? (
              <iframe className="request-preview-frame" src={resolvedUrl} title={fileName || "preview"} />
            ) : null}
            {!loading && !error && kind === "text" ? (
              <pre className="request-preview-text">{resolvedText || "Файл пуст."}</pre>
            ) : null}
            {kind === "none" ? <p className="request-preview-note">Для этого типа файла доступно только открытие или скачивание.</p> : null}
          </div>
        </div>
      </Overlay>
    );
  }

  function ClientHelpModal({
    open,
    status,
    loadingType,
    lawyerChangeReason,
    curatorBlocked,
    lawyerChangeBlocked,
    onClose,
    onReasonChange,
    onSubmitCurator,
    onSubmitLawyerChange,
  }) {
    return (
      <div className={"overlay" + (open ? " open" : "")} id="client-help-overlay" onClick={(event) => event.target.id === "client-help-overlay" && onClose()}>
        <div className="modal client-help-modal" onClick={(event) => event.stopPropagation()}>
          <div className="modal-head">
            <div>
              <h3 id="client-help-title">Помощь по заявке</h3>
            </div>
            <button className="close" type="button" id="client-help-close" onClick={onClose} aria-label="Закрыть">
              ×
            </button>
          </div>
          <div className="client-help-stack">
            <section className="client-help-block">
              <p className="client-help-description">
                Если нужна дополнительная поддержка по делу, можно обратиться к куратору. Запрос отправляется администратору платформы.
              </p>
              <button
                className="btn secondary btn-sm"
                id="cabinet-curator-request-open"
                type="button"
                disabled={curatorBlocked || loadingType === "CURATOR_CONTACT"}
                onClick={onSubmitCurator}
              >
                {loadingType === "CURATOR_CONTACT" ? "Отправка..." : "Обратиться к куратору"}
              </button>
            </section>
            <section className="client-help-block">
              <p className="client-help-description">
                Если текущий юрист не устраивает, можно запросить его смену. Укажите причину для администратора.
              </p>
              <div className="field">
                <label htmlFor="service-request-body">Причина смены юриста</label>
                <textarea
                  id="service-request-body"
                  value={lawyerChangeReason}
                  onChange={onReasonChange}
                  maxLength={4000}
                  placeholder="Опишите причину"
                  disabled={lawyerChangeBlocked || loadingType === "LAWYER_CHANGE_REQUEST"}
                />
              </div>
              <button
                className="btn secondary btn-sm"
                id="cabinet-lawyer-change-open"
                type="button"
                disabled={lawyerChangeBlocked || loadingType === "LAWYER_CHANGE_REQUEST"}
                onClick={onSubmitLawyerChange}
              >
                {loadingType === "LAWYER_CHANGE_REQUEST" ? "Отправка..." : "Запросить смену"}
              </button>
            </section>
            <StatusLine status={status} />
          </div>
        </div>
      </div>
    );
  }

  function RequestPickerModal({
    open,
    loading,
    requests,
    activeTrack,
    onClose,
    onReload,
    onSelect,
  }) {
    const rows = Array.isArray(requests) ? requests : [];
    return (
      <Overlay open={open} id="client-request-picker-overlay" onClose={(event) => event.target.id === "client-request-picker-overlay" && onClose()}>
        <div className="modal client-request-picker-modal" onClick={(event) => event.stopPropagation()}>
          <div className="modal-head">
            <div>
              <h3>Выбор заявки</h3>
              <p className="muted client-request-picker-subtitle">Откройте нужную заявку из списка</p>
            </div>
            <div className="modal-head-actions">
              <button
                className="icon-btn file-action-btn"
                type="button"
                data-tooltip="Обновить список"
                aria-label="Обновить список"
                onClick={onReload}
                disabled={loading}
              >
                <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false">
                  <path
                    d="M12 4a8 8 0 0 1 7.7 5.8 1 1 0 0 1-1.92.56A6 6 0 1 0 16.7 16H15a1 1 0 1 1 0-2h4a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0v-1.18A8 8 0 1 1 12 4z"
                    fill="currentColor"
                  />
                </svg>
              </button>
              <button className="close" type="button" onClick={onClose} aria-label="Закрыть">
                ×
              </button>
            </div>
          </div>
          <ul className="simple-list client-request-picker-list" id="client-request-picker-list">
            {rows.length ? (
              rows.map((row, index) => {
                const track = String(row?.track_number || "").trim();
                const isActive = track && track === String(activeTrack || "").trim();
                const hasUpdates = Number(row?.viewer_unread_total || 0) > 0 || Boolean(row?.client_has_unread_updates);
                const statusName = String(row?.status_name || statusLabel(row?.status_code) || "-");
                return (
                  <li
                    key={String(row?.id || track || "row-" + String(index))}
                    className={"client-request-picker-item" + (isActive ? " active" : "") + (hasUpdates ? " has-updates" : "")}
                  >
                    <button
                      type="button"
                      className="client-request-picker-btn"
                      onClick={() => onSelect(track)}
                      disabled={!track || loading}
                      aria-label={"Открыть заявку " + (track || "")}
                    >
                      <div className="client-request-picker-head">
                        <span className="client-request-picker-track">{track || "Без номера"}</span>
                        <span className={"client-request-news-dot" + (hasUpdates ? " active" : "")} aria-hidden="true" />
                      </div>
                      <div className="client-request-picker-meta">
                        <span className="client-request-picker-status">{statusName}</span>
                        <span className="client-request-picker-updated">{fmtShortDateTime(row?.updated_at)}</span>
                      </div>
                    </button>
                  </li>
                );
              })
            ) : (
              <li className="muted">{loading ? "Загрузка списка..." : "Заявок не найдено"}</li>
            )}
          </ul>
          <div className="client-request-picker-actions">
            <button className="btn secondary btn-sm" type="button" onClick={onClose}>
              Отмена
            </button>
          </div>
        </div>
      </Overlay>
    );
  }

  function App() {
    const UPLOAD_MAX_ATTEMPTS = 4;
    const [requestModal, setRequestModal] = useState(createRequestModalState());
    const [requestsList, setRequestsList] = useState([]);
    const [activeTrack, setActiveTrack] = useState("");
    const [requestPickerModal, setRequestPickerModal] = useState({ open: false, loading: false });
    const [status, setStatus] = useState({ message: "", kind: "" });
    const [serviceRequests, setServiceRequests] = useState([]);
    const [clientHelpModal, setClientHelpModal] = useState({
      open: false,
      loadingType: "",
      lawyerChangeReason: "",
      status: { message: "", kind: "" },
    });

    const setPageStatus = useCallback((message, kind) => {
      setStatus({ message: String(message || ""), kind: kind || "" });
    }, []);

    const apiError = (data, fallback) => {
      if (data && typeof data.detail === "string" && data.detail.trim()) return data.detail;
      return fallback;
    };

    const parseJsonSafe = async (response) => {
      try {
        return await response.json();
      } catch (_) {
        return null;
      }
    };

    const apiJson = useCallback(async (url, options, fallbackMessage) => {
      const response = await fetch(url, options || undefined);
      const data = await parseJsonSafe(response);
      if (response.status === 401 || response.status === 403) {
        window.location.href = "/";
        throw new Error("Нет доступа");
      }
      if (!response.ok) {
        const error = new Error(apiError(data, fallbackMessage || "Ошибка запроса"));
        error.httpStatus = Number(response.status || 0);
        throw error;
      }
      return data;
    }, []);

    const buildStorageUploadError = useCallback(async (response, fallbackMessage) => {
      const base = String(fallbackMessage || "Ошибка передачи файла в хранилище");
      const status = Number(response?.status || 0);
      const statusText = String(response?.statusText || "").trim();
      let details = "";
      try {
        details = String((await response.text()) || "").replace(/\s+/g, " ").trim();
      } catch (_) {
        details = "";
      }
      if (details.length > 180) details = details.slice(0, 180) + "...";
      const parts = [];
      if (status > 0) parts.push("HTTP " + status + (statusText ? " " + statusText : ""));
      if (details) parts.push(details);
      return parts.length ? base + " (" + parts.join("; ") + ")" : base;
    }, []);

    const wait = useCallback(async (ms) => {
      await new Promise((resolve) => window.setTimeout(resolve, Math.max(0, Number(ms) || 0)));
    }, []);

    const nextUploadRetryDelayMs = useCallback((attempt) => {
      const base = Math.min(1200 * Math.pow(2, Math.max(0, Number(attempt || 1) - 1)), 7000);
      const jitter = Math.floor(Math.random() * 250);
      return base + jitter;
    }, []);

    const isRetryableUploadError = useCallback((error) => {
      const status = Number(error?.httpStatus || error?.status || 0);
      if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
      if (status > 0) return false;
      const message = String(error?.message || "").toLowerCase();
      if (!message) return true;
      return (
        message.includes("networkerror") ||
        message.includes("failed to fetch") ||
        message.includes("load failed") ||
        message.includes("network request failed") ||
        message.includes("timeout")
      );
    }, []);

    const runUploadStepWithRetry = useCallback(
      async (label, action) => {
        let lastError = null;
        let attemptsUsed = 0;
        for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt += 1) {
          attemptsUsed = attempt;
          try {
            return await action(attempt);
          } catch (error) {
            lastError = error;
            const canRetry = attempt < UPLOAD_MAX_ATTEMPTS && isRetryableUploadError(error);
            if (!canRetry) break;
            await wait(nextUploadRetryDelayMs(attempt));
          }
        }
        const reason = String(lastError?.message || "Ошибка сети");
        throw new Error(label + ": " + reason + " (попыток: " + attemptsUsed + ")");
      },
      [UPLOAD_MAX_ATTEMPTS, isRetryableUploadError, nextUploadRetryDelayMs, wait]
    );

    const uploadPublicRequestAttachment = useCallback(async (file, extra = {}) => {
      const requestId = String(requestModal.requestId || "").trim();
      if (!requestId) throw new Error("Не выбрана заявка");
      const mimeType = String(file?.type || "application/octet-stream");
      const initData = await runUploadStepWithRetry("Не удалось начать загрузку файла", async () => {
        return apiJson(
          "/api/public/uploads/init",
          {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              file_name: file.name,
              mime_type: mimeType,
              size_bytes: file.size,
              scope: "REQUEST_ATTACHMENT",
              request_id: requestId,
            }),
          },
          "Не удалось начать загрузку файла"
        );
      });
      await runUploadStepWithRetry("Ошибка передачи файла в хранилище", async () => {
        const putResponse = await fetch(initData.presigned_url, {
          method: "PUT",
          headers: { "Content-Type": mimeType },
          body: file,
        });
        if (putResponse.ok) return null;
        const error = new Error(await buildStorageUploadError(putResponse, "Ошибка передачи файла в хранилище"));
        error.httpStatus = Number(putResponse.status || 0);
        throw error;
      });
      const completeData = await runUploadStepWithRetry("Не удалось завершить загрузку файла", async () => {
        return apiJson(
          "/api/public/uploads/complete",
          {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              key: initData.key,
              file_name: file.name,
              mime_type: mimeType,
              size_bytes: file.size,
              scope: "REQUEST_ATTACHMENT",
              request_id: requestId,
              message_id: extra?.message_id || null,
            }),
          },
          "Не удалось завершить загрузку файла"
        );
      });
      return completeData;
    }, [apiJson, buildStorageUploadError, requestModal.requestId, runUploadStepWithRetry]);

    const hydratePublicMessageBodies = useCallback(
      async (trackNumber, rows) => {
        const track = String(trackNumber || "").trim().toUpperCase();
        const ids = collectDeferredMessageIds(rows);
        if (!track || !ids.length) return null;
        try {
          const payload = await apiJson(
            "/api/public/chat/requests/" + encodeURIComponent(track) + "/message-bodies",
            {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ ids }),
            },
            "Не удалось загрузить тексты сообщений"
          );
          const nextRows = Array.isArray(payload?.rows) ? payload.rows : [];
          if (!nextRows.length) return payload || null;
          setRequestModal((prev) => {
            if (String(prev.trackNumber || "").trim().toUpperCase() !== track) return prev;
            return {
              ...prev,
              messages: mergeRowsById(prev.messages, nextRows),
            };
          });
          return payload || null;
        } catch (_) {
          return null;
        }
      },
      [apiJson]
    );

    const loadRequestWorkspace = useCallback(
      async (trackNumber, showLoading) => {
        const track = String(trackNumber || "").trim().toUpperCase();
        if (!track) return;
        if (showLoading) {
          setRequestModal((prev) => ({ ...prev, loading: true }));
        }
        const [requestData, messagesData, attachmentsData, invoicesData, statusRouteData, serviceRequestsData] = await Promise.all([
          apiJson("/api/public/requests/" + encodeURIComponent(track), null, "Не удалось открыть заявку"),
          apiJson(
            "/api/public/chat/requests/" + encodeURIComponent(track) + "/messages-window?include_body=false",
            null,
            "Не удалось загрузить сообщения"
          ),
          apiJson("/api/public/requests/" + encodeURIComponent(track) + "/attachments", null, "Не удалось загрузить файлы"),
          apiJson("/api/public/requests/" + encodeURIComponent(track) + "/invoices", null, "Не удалось загрузить счета"),
          apiJson("/api/public/requests/" + encodeURIComponent(track) + "/status-route", null, "Не удалось загрузить маршрут статусов"),
          apiJson("/api/public/requests/" + encodeURIComponent(track) + "/service-requests", null, "Не удалось загрузить обращения"),
        ]);

        const invoices = Array.isArray(invoicesData) ? invoicesData : [];
        const paidInvoices = invoices.filter((item) => String(item?.status || "").toUpperCase() === "PAID");
        const paidTotal = paidInvoices.reduce((acc, item) => {
          const amount = Number(item?.amount || 0);
          return Number.isFinite(amount) ? acc + amount : acc;
        }, 0);
        const lastPaidAt = paidInvoices.reduce((latest, item) => {
          const raw = String(item?.paid_at || "").trim();
          if (!raw) return latest;
          if (!latest) return raw;
          const currentTs = new Date(raw).getTime();
          const latestTs = new Date(latest).getTime();
          return Number.isFinite(currentTs) && currentTs > latestTs ? raw : latest;
        }, "");

        setActiveTrack(track);
        setServiceRequests(Array.isArray(serviceRequestsData) ? serviceRequestsData : []);
        setRequestModal((prev) => ({
          ...prev,
          loading: false,
          requestId: String(requestData?.id || ""),
          trackNumber: String(requestData?.track_number || track),
          requestData: requestData || null,
          financeSummary: {
            request_cost: requestData?.request_cost ?? null,
            effective_rate: requestData?.effective_rate ?? null,
            paid_total: Math.round((paidTotal + Number.EPSILON) * 100) / 100,
            last_paid_at: lastPaidAt || requestData?.paid_at || null,
          },
          statusRouteNodes: Array.isArray(statusRouteData?.nodes) ? statusRouteData.nodes : [],
          statusHistory: Array.isArray(statusRouteData?.history) ? statusRouteData.history : [],
          availableStatuses: [],
          currentImportantDateAt: String(statusRouteData?.current_important_date_at || requestData?.important_date_at || ""),
          invoices,
          messages: Array.isArray(messagesData?.rows) ? messagesData.rows : [],
          messagesHasMore: Boolean(messagesData?.has_more),
          messagesLoadingMore: false,
          messagesLoadedCount: Array.isArray(messagesData?.rows) ? messagesData.rows.length : 0,
          messagesTotal: Number(messagesData?.total || (Array.isArray(messagesData?.rows) ? messagesData.rows.length : 0)),
          attachments: Array.isArray(attachmentsData) ? attachmentsData : [],
          fileUploading: false,
        }));
        void hydratePublicMessageBodies(track, Array.isArray(messagesData?.rows) ? messagesData.rows : []);
      },
      [apiJson, hydratePublicMessageBodies]
    );

    const loadOlderPublicMessages = useCallback(async () => {
      const track = String(activeTrack || requestModal.trackNumber || "").trim().toUpperCase();
      const cursor = getOldestMessageCursor(requestModal.messages);
      if (!track || requestModal.messagesLoadingMore || !requestModal.messagesHasMore) return null;
      setRequestModal((prev) => ({ ...prev, messagesLoadingMore: true }));
      try {
        const query = new URLSearchParams({ include_body: "false" });
        if (cursor?.beforeId && cursor?.beforeCreatedAt) {
          query.set("before_id", cursor.beforeId);
          query.set("before_created_at", cursor.beforeCreatedAt);
        } else {
          query.set("before_count", String(Number(requestModal.messagesLoadedCount || 0)));
        }
        const payload = await apiJson(
          "/api/public/chat/requests/" + encodeURIComponent(track) + "/messages-window?" + query.toString(),
          null,
          "Не удалось загрузить историю сообщений"
        );
        const nextRows = Array.isArray(payload?.rows) ? payload.rows : [];
        setRequestModal((prev) => ({
          ...prev,
          messagesLoadingMore: false,
          ...(function () {
            const merged = mergeRowsById(nextRows, prev.messages);
            return {
              messages: merged,
              messagesHasMore: Boolean(payload?.has_more),
              messagesLoadedCount: merged.length,
              messagesTotal: Number(prev.messagesTotal || merged.length || 0),
            };
          })(),
        }));
        void hydratePublicMessageBodies(track, nextRows);
        return payload || null;
      } catch (error) {
        setRequestModal((prev) => ({ ...prev, messagesLoadingMore: false }));
        setPageStatus(error?.message || "Не удалось загрузить историю сообщений", "error");
        return null;
      }
    }, [
      activeTrack,
      apiJson,
      requestModal.messagesHasMore,
      requestModal.messagesLoadedCount,
      requestModal.messagesLoadingMore,
      requestModal.trackNumber,
      setPageStatus,
      hydratePublicMessageBodies,
    ]);

    const refreshRequestsList = useCallback(async () => {
      const data = await apiJson("/api/public/requests/my", null, "Не удалось загрузить список заявок");
      const rows = Array.isArray(data?.rows) ? data.rows : [];
      setRequestsList(rows);
      return rows;
    }, [apiJson]);

    const loadMyRequests = useCallback(
      async (preferredTrack) => {
        const rows = await refreshRequestsList();
        if (!rows.length) {
          setRequestModal((prev) => ({
            ...prev,
            loading: false,
            requestId: null,
            requestData: null,
            trackNumber: "",
            financeSummary: null,
            invoices: [],
            statusRouteNodes: [],
            statusHistory: [],
            messages: [],
            messagesHasMore: false,
            messagesLoadingMore: false,
            messagesLoadedCount: 0,
            messagesTotal: 0,
            attachments: [],
            fileUploading: false,
            selectedFiles: [],
            messageDraft: "",
          }));
          setServiceRequests([]);
          setPageStatus("По вашему номеру пока нет заявок.", "");
          return;
        }
        const tracks = rows.map((row) => String(row.track_number || "").trim()).filter(Boolean);
        const selected = tracks.includes(String(preferredTrack || "").trim().toUpperCase())
          ? String(preferredTrack || "").trim().toUpperCase()
          : tracks[0];
        await loadRequestWorkspace(selected, true);
      },
      [loadRequestWorkspace, refreshRequestsList, setPageStatus]
    );

    const openRequestPicker = useCallback(() => {
      setRequestPickerModal({ open: true, loading: true });
      void refreshRequestsList()
        .catch((error) => setPageStatus(error?.message || "Не удалось загрузить список заявок", "error"))
        .finally(() => {
          setRequestPickerModal((prev) => ({ ...prev, loading: false }));
        });
    }, [refreshRequestsList, setPageStatus]);

    const closeRequestPicker = useCallback(() => {
      setRequestPickerModal((prev) => ({ ...prev, open: false }));
    }, []);

    const reloadRequestPicker = useCallback(() => {
      setRequestPickerModal((prev) => ({ ...prev, loading: true }));
      void refreshRequestsList()
        .catch((error) => setPageStatus(error?.message || "Не удалось обновить список заявок", "error"))
        .finally(() => {
          setRequestPickerModal((prev) => ({ ...prev, loading: false }));
        });
    }, [refreshRequestsList, setPageStatus]);

    const selectRequestFromPicker = useCallback(
      async (trackNumber) => {
        const track = String(trackNumber || "").trim().toUpperCase();
        if (!track) return;
        setRequestPickerModal((prev) => ({ ...prev, loading: true }));
        try {
          await loadRequestWorkspace(track, true);
          await refreshRequestsList();
          setRequestPickerModal({ open: false, loading: false });
        } catch (error) {
          setRequestPickerModal((prev) => ({ ...prev, loading: false }));
          setPageStatus(error?.message || "Не удалось открыть заявку", "error");
        }
      },
      [loadRequestWorkspace, refreshRequestsList, setPageStatus]
    );

    const updateMessageDraft = useCallback((event) => {
      const value = event?.target?.value || "";
      setRequestModal((prev) => ({ ...prev, messageDraft: value }));
    }, []);

    const appendFiles = useCallback((files) => {
      const list = Array.isArray(files) ? files.filter(Boolean) : [];
      if (!list.length) return;
      setRequestModal((prev) => {
        const existing = Array.isArray(prev.selectedFiles) ? prev.selectedFiles : [];
        const next = [...existing];
        list.forEach((file) => {
          const duplicate = next.some(
            (item) =>
              item &&
              item.name === file.name &&
              Number(item.size || 0) === Number(file.size || 0) &&
              Number(item.lastModified || 0) === Number(file.lastModified || 0)
          );
          if (!duplicate) next.push(file);
        });
        return { ...prev, selectedFiles: next };
      });
    }, []);

    const removeFile = useCallback((index) => {
      setRequestModal((prev) => {
        const files = Array.isArray(prev.selectedFiles) ? [...prev.selectedFiles] : [];
        files.splice(index, 1);
        return { ...prev, selectedFiles: files };
      });
    }, []);

    const clearFiles = useCallback(() => {
      setRequestModal((prev) => ({ ...prev, selectedFiles: [] }));
    }, []);

    const downloadPublicInvoicePdf = useCallback(
      async (row) => {
        const url = String(row?.download_url || "").trim();
        if (!url) {
          setPageStatus("Ссылка на PDF счета недоступна.", "error");
          return;
        }
        try {
          setPageStatus("Скачиваем PDF...", "");
          const response = await fetch(url, { credentials: "same-origin" });
          if (!response.ok) {
            const text = await response.text();
            let payload = {};
            try {
              payload = text ? JSON.parse(text) : {};
            } catch (_) {
              payload = { raw: text };
            }
            const message = payload.detail || payload.error || payload.raw || ("HTTP " + response.status);
            throw new Error(String(message));
          }
          const blob = await response.blob();
          const fileName = String(row?.invoice_number || "invoice") + ".pdf";
          const fileUrl = URL.createObjectURL(blob);
          const link = document.createElement("a");
          link.href = fileUrl;
          link.download = fileName;
          document.body.appendChild(link);
          link.click();
          link.remove();
          URL.revokeObjectURL(fileUrl);
          setPageStatus("PDF скачан", "ok");
        } catch (error) {
          setPageStatus("Ошибка скачивания: " + (error?.message || "неизвестная ошибка"), "error");
        }
      },
      [setPageStatus]
    );

    const submitMessage = useCallback(
      async (event) => {
        if (event && typeof event.preventDefault === "function") event.preventDefault();
        const track = String(activeTrack || "").trim();
        const requestId = String(requestModal.requestId || "").trim();
        if (!track || !requestId) {
          setPageStatus("Сначала выберите заявку.", "error");
          return;
        }
        const body = String(requestModal.messageDraft || "").trim();
        const files = Array.isArray(requestModal.selectedFiles) ? requestModal.selectedFiles : [];
        if (!body && !files.length) return;
        try {
          setRequestModal((prev) => ({ ...prev, fileUploading: true }));
          setPageStatus(files.length ? "Отправка сообщения и файлов..." : "Отправка сообщения...", "");

          let messageId = null;
          if (body) {
            const messageData = await apiJson(
              "/api/public/chat/requests/" + encodeURIComponent(track) + "/messages",
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ body }),
              },
              "Не удалось отправить сообщение"
            );
            messageId = String(messageData?.id || "").trim() || null;
          }
          for (const file of files) {
            await uploadPublicRequestAttachment(file, { message_id: messageId });
          }
          setRequestModal((prev) => ({ ...prev, messageDraft: "", selectedFiles: [], fileUploading: false }));
          await loadRequestWorkspace(track, false);
          if (body && files.length) setPageStatus("Сообщение и файлы отправлены.", "ok");
          else if (files.length) setPageStatus(files.length === 1 ? "Файл загружен." : "Файлы загружены.", "ok");
          else setPageStatus("Сообщение отправлено.", "ok");
        } catch (error) {
          setRequestModal((prev) => ({ ...prev, fileUploading: false }));
          setPageStatus(error?.message || "Ошибка отправки сообщения", "error");
        }
      },
      [activeTrack, apiJson, loadRequestWorkspace, requestModal.messageDraft, requestModal.requestId, requestModal.selectedFiles, setPageStatus, uploadPublicRequestAttachment]
    );

    const loadRequestDataBatch = useCallback(
      async (messageId) => {
        const track = String(activeTrack || "").trim();
        if (!track || !messageId) throw new Error("Не выбрана заявка");
        return apiJson(
          "/api/public/chat/requests/" + encodeURIComponent(track) + "/data-requests/" + encodeURIComponent(String(messageId)),
          null,
          "Не удалось открыть запрос данных"
        );
      },
      [activeTrack, apiJson]
    );

    const saveRequestDataValues = useCallback(
      async ({ message_id, items }) => {
        const track = String(activeTrack || "").trim();
        const messageId = String(message_id || "").trim();
        if (!track || !messageId) throw new Error("Не выбрана заявка");
        await apiJson(
          "/api/public/chat/requests/" + encodeURIComponent(track) + "/data-requests/" + encodeURIComponent(messageId),
          {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ items: Array.isArray(items) ? items : [] }),
          },
          "Не удалось сохранить данные"
        );
        await loadRequestWorkspace(track, false);
      },
      [activeTrack, apiJson, loadRequestWorkspace]
    );

    const probeLiveState = useCallback(
      async ({ cursor } = {}) => {
        const track = String(activeTrack || "").trim();
        if (!track) return { has_updates: false, typing: [], cursor: null };
        const query = cursor ? "?cursor=" + encodeURIComponent(String(cursor)) : "";
        const payload = await apiJson(
          "/api/public/chat/requests/" + encodeURIComponent(track) + "/live" + query,
          null,
          "Не удалось получить live-обновления чата"
        );
        if (payload && payload.has_updates) {
          const nextMessages = Array.isArray(payload?.messages) ? payload.messages : [];
          const nextAttachments = Array.isArray(payload?.attachments) ? payload.attachments : [];
          if (nextMessages.length || nextAttachments.length) {
            setRequestModal((prev) => {
              const mergedMessages = mergeRowsById(prev.messages, nextMessages);
              const previousCount = Array.isArray(prev.messages) ? prev.messages.length : 0;
              const addedCount = Math.max(0, mergedMessages.length - previousCount);
              return {
                ...prev,
                messages: mergedMessages,
                messagesLoadedCount: Number(prev.messagesLoadedCount || previousCount) + addedCount,
                messagesTotal: Number(prev.messagesTotal || previousCount) + addedCount,
                attachments: mergeRowsById(prev.attachments, nextAttachments),
              };
            });
          }
        }
        return payload || { has_updates: false, typing: [], cursor: null };
      },
      [activeTrack, apiJson]
    );

    const setTypingSignal = useCallback(
      async ({ typing } = {}) => {
        const track = String(activeTrack || "").trim();
        if (!track) return { status: "skipped", typing: false };
        return apiJson(
          "/api/public/chat/requests/" + encodeURIComponent(track) + "/typing",
          {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ typing: Boolean(typing) }),
          },
          "Не удалось обновить статус набора"
        );
      },
      [activeTrack, apiJson]
    );

    const openClientHelpModal = useCallback(() => {
      setClientHelpModal((prev) => ({
        ...prev,
        open: true,
        status: { message: "", kind: "" },
      }));
    }, []);

    const closeClientHelpModal = useCallback(() => {
      setClientHelpModal((prev) => ({
        ...prev,
        open: false,
        loadingType: "",
        status: { message: "", kind: "" },
      }));
    }, []);

    const normalizedCurrentLawyerId = String(requestModal.requestData?.assigned_lawyer_id || "")
      .trim()
      .toLowerCase();

    const serviceRequestState = useMemo(() => {
      const rows = Array.isArray(serviceRequests) ? serviceRequests : [];
      const curatorRows = rows.filter((item) => String(item?.type || "").toUpperCase() === "CURATOR_CONTACT");
      const lawyerRows = rows.filter((item) => String(item?.type || "").toUpperCase() === "LAWYER_CHANGE_REQUEST");
      const latestLawyerChange = lawyerRows[0] || null;
      const hasCuratorRequest = curatorRows.length > 0;

      let lawyerChangeDisabledByState = false;
      if (latestLawyerChange) {
        const hasLawyerSnapshot = Object.prototype.hasOwnProperty.call(latestLawyerChange, "assigned_lawyer_id");
        if (hasLawyerSnapshot) {
          const requestedForLawyer = String(latestLawyerChange?.assigned_lawyer_id || "")
            .trim()
            .toLowerCase();
          lawyerChangeDisabledByState = requestedForLawyer === normalizedCurrentLawyerId;
        } else {
          const statusCode = String(latestLawyerChange?.status || "").toUpperCase();
          lawyerChangeDisabledByState = statusCode === "NEW" || statusCode === "IN_PROGRESS";
        }
      }

      return {
        hasCuratorRequest,
        lawyerChangeDisabledByState,
      };
    }, [normalizedCurrentLawyerId, serviceRequests]);

    const canInteract = Boolean(requestModal.requestData && !requestModal.loading);
    const curatorBlocked = !canInteract || serviceRequestState.hasCuratorRequest;
    const lawyerChangeBlocked = !canInteract || serviceRequestState.lawyerChangeDisabledByState;

    const submitServiceRequest = useCallback(
      async ({ type, body }) => {
        const track = String(activeTrack || "").trim();
        const requestType = String(type || "").trim().toUpperCase();
        const message = String(body || "").trim();
        if (!track) {
          setClientHelpModal((prev) => ({ ...prev, status: { message: "Сначала выберите заявку.", kind: "error" } }));
          return;
        }
        if (!requestType) {
          setClientHelpModal((prev) => ({ ...prev, status: { message: "Не указан тип запроса.", kind: "error" } }));
          return;
        }
        if (message.length < 3) {
          setClientHelpModal((prev) => ({ ...prev, status: { message: "Сообщение должно содержать минимум 3 символа.", kind: "error" } }));
          return;
        }
        try {
          setClientHelpModal((prev) => ({ ...prev, loadingType: requestType, status: { message: "", kind: "" } }));
          await apiJson(
            "/api/public/requests/" + encodeURIComponent(track) + "/service-requests",
            {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ type: requestType, body: message }),
            },
            "Не удалось отправить обращение"
          );
          await loadRequestWorkspace(track, false);
          setPageStatus("Обращение отправлено.", "ok");
          setClientHelpModal((prev) => ({
            ...prev,
            loadingType: "",
            status: { message: "Обращение отправлено.", kind: "ok" },
            lawyerChangeReason: requestType === "LAWYER_CHANGE_REQUEST" ? "" : prev.lawyerChangeReason,
          }));
        } catch (error) {
          setClientHelpModal((prev) => ({
            ...prev,
            loadingType: "",
            status: { message: error?.message || "Не удалось отправить обращение", kind: "error" },
          }));
        }
      },
      [activeTrack, apiJson, loadRequestWorkspace, setPageStatus]
    );

    useEffect(() => {
      const params = new URLSearchParams(window.location.search);
      const preferredTrack = String(params.get("track") || "").trim().toUpperCase();
      void loadMyRequests(preferredTrack).catch((error) => {
        setPageStatus(error?.message || "Не удалось открыть страницу клиента", "error");
      });
    }, [loadMyRequests, setPageStatus]);

    const summary = requestModal.requestData || null;
    const summaryStatusName = String(summary?.status_name || statusLabel(summary?.status_code) || "-");
    const viewerFullName = useMemo(() => {
      const fullName = String(requestModal.requestData?.client_name || "").trim();
      return fullName || "Клиент";
    }, [requestModal.requestData?.client_name]);
    const hasAnyUnreadUpdates = useMemo(
      () => requestsList.some((row) => Number(row?.viewer_unread_total || 0) > 0 || Boolean(row?.client_has_unread_updates)),
      [requestsList]
    );

    return (
      <div className="client-page-shell">
        <main className="main client-main">
          <div className="topbar client-topbar">
            <div className="client-topbar-copy">
              <div className="client-title-row">
                <img className="brand-mark" src="/brand-mark.svg" alt="" width="24" height="24" />
                <h1>
                  <span>Личный кабинет</span>
                  <span className="client-title-separator" aria-hidden="true">
                    •
                  </span>
                  <span className="client-title-user">{viewerFullName}</span>
                </h1>
              </div>
              <div className="client-help-inline">
                <p className="muted">Мы рады вам помочь</p>
                <button
                  className="icon-btn workspace-head-icon"
                  id="cabinet-help-open"
                  type="button"
                  data-tooltip="Помощь и обращения"
                  aria-label="Помощь и обращения"
                  disabled={!canInteract}
                  onClick={openClientHelpModal}
                >
                  ?
                </button>
              </div>
            </div>
          </div>

          <section className="section active client-section">
            <div className="section-head">
              <div>
                <h2>Мои заявки</h2>
              </div>
            </div>
            <div className="client-request-toolbar">
              <button
                className="btn secondary client-request-picker-trigger"
                id="client-request-open"
                type="button"
                onClick={openRequestPicker}
                disabled={requestModal.loading || !requestsList.length}
              >
                <span className="client-request-picker-trigger-label">Заявка</span>
                <span className="client-request-picker-trigger-track">{activeTrack || "Выбрать"}</span>
                {hasAnyUnreadUpdates ? <span className="client-request-news-dot active" aria-hidden="true" /> : null}
              </button>
            </div>

            <div className="client-summary" id="cabinet-summary" hidden={!summary}>
              <div className="client-summary-row">
                <div className="client-summary-chips">
                  <span className="client-summary-chip client-summary-chip-status">
                    Статус: <span id="cabinet-request-status">{summary ? summaryStatusName : "-"}</span>
                  </span>
                  <span className="client-summary-chip client-summary-chip-topic">
                    Тема: <span id="cabinet-request-topic">{summary ? String(summary.topic_name || summary.topic_code || "-") : "-"}</span>
                  </span>
                  <span className="client-summary-chip client-summary-chip-lawyer">
                    Юрист: <span>{summary ? String(summary.assigned_lawyer_name || summary.assigned_lawyer_id || "Не назначен") : "-"}</span>
                  </span>
                </div>
                <div className="client-summary-dates">
                  <span>
                    Обновлена: <b id="cabinet-request-updated">{summary ? fmtShortDateTime(summary.updated_at) : "-"}</b>
                  </span>
                </div>
              </div>
            </div>

            <RequestWorkspace
              viewerRole="CLIENT"
              viewerUserId=""
              loading={requestModal.loading}
              trackNumber={requestModal.trackNumber}
              requestData={requestModal.requestData}
              financeSummary={requestModal.financeSummary}
              invoices={requestModal.invoices || []}
              statusRouteNodes={requestModal.statusRouteNodes || []}
              statusHistory={requestModal.statusHistory || []}
              availableStatuses={[]}
              currentImportantDateAt={requestModal.currentImportantDateAt || ""}
              pendingStatusChangePreset={null}
              messages={requestModal.messages || []}
              messagesHasMore={Boolean(requestModal.messagesHasMore)}
              messagesLoadingMore={Boolean(requestModal.messagesLoadingMore)}
              attachments={requestModal.attachments || []}
              messageDraft={requestModal.messageDraft || ""}
              selectedFiles={requestModal.selectedFiles || []}
              fileUploading={Boolean(requestModal.fileUploading)}
              status={status}
              onMessageChange={updateMessageDraft}
              onSendMessage={submitMessage}
              onLoadOlderMessages={loadOlderPublicMessages}
              onFilesSelect={appendFiles}
              onRemoveSelectedFile={removeFile}
              onClearSelectedFiles={clearFiles}
              onLoadRequestDataBatch={loadRequestDataBatch}
              onSaveRequestDataValues={saveRequestDataValues}
              onUploadRequestAttachment={uploadPublicRequestAttachment}
              onChangeStatus={() => Promise.resolve(null)}
              onDownloadInvoicePdf={downloadPublicInvoicePdf}
              onLiveProbe={probeLiveState}
              onTypingSignal={setTypingSignal}
              AttachmentPreviewModalComponent={AttachmentPreviewModal}
              StatusLineComponent={StatusLine}
              domIds={{
                messagesList: "cabinet-messages",
                filesList: "cabinet-files",
                messageBody: "cabinet-chat-body",
                sendButton: "cabinet-chat-send",
                fileInput: "cabinet-file-input",
                dataRequestOverlay: "data-request-overlay",
                dataRequestItems: "data-request-items",
                dataRequestStatus: "data-request-status",
                dataRequestSave: "data-request-save",
              }}
            />
          </section>
          <p className="status" id="client-page-status">{status.message}</p>
        </main>
        <RequestPickerModal
          open={requestPickerModal.open}
          loading={requestPickerModal.loading}
          requests={requestsList}
          activeTrack={activeTrack}
          onClose={closeRequestPicker}
          onReload={reloadRequestPicker}
          onSelect={selectRequestFromPicker}
        />
        <ClientHelpModal
          open={clientHelpModal.open}
          status={clientHelpModal.status}
          loadingType={clientHelpModal.loadingType}
          lawyerChangeReason={clientHelpModal.lawyerChangeReason}
          curatorBlocked={curatorBlocked}
          lawyerChangeBlocked={lawyerChangeBlocked}
          onClose={closeClientHelpModal}
          onReasonChange={(event) =>
            setClientHelpModal((prev) => ({
              ...prev,
              lawyerChangeReason: event.target.value,
              status: { message: "", kind: "" },
            }))
          }
          onSubmitCurator={() => submitServiceRequest({ type: "CURATOR_CONTACT", body: "Прошу подключить куратора к текущей заявке." })}
          onSubmitLawyerChange={() => submitServiceRequest({ type: "LAWYER_CHANGE_REQUEST", body: clientHelpModal.lawyerChangeReason })}
        />
        <GlobalTooltipLayer />
      </div>
    );
  }

  const root = document.getElementById("client-root");
  if (root) {
    ReactDOM.createRoot(root).render(<App />);
  }
})();
